diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index 364869ba..77f9868b 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -13,7 +13,7 @@ import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/adaptive.dart'; -import 'package:neon/src/utils/confirmation_dialog.dart'; +import 'package:neon/src/utils/dialog.dart'; import 'package:neon/src/widgets/error.dart'; import 'package:nextcloud/provisioning_api.dart' as provisioning_api; diff --git a/packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart b/packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart index b26b5451..a00b29a4 100644 --- a/packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart +++ b/packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart @@ -7,7 +7,7 @@ import 'package:neon/src/settings/widgets/option_settings_tile.dart'; import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/theme/dialog.dart'; -import 'package:neon/src/utils/confirmation_dialog.dart'; +import 'package:neon/src/utils/dialog.dart'; @internal class NextcloudAppSettingsPage extends StatelessWidget { diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index c2eb3098..f92703f5 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -19,7 +19,7 @@ import 'package:neon/src/settings/widgets/text_settings_tile.dart'; import 'package:neon/src/theme/branding.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/adaptive.dart'; -import 'package:neon/src/utils/confirmation_dialog.dart'; +import 'package:neon/src/utils/dialog.dart'; import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/save_file.dart'; diff --git a/packages/neon/neon/lib/src/utils/dialog.dart b/packages/neon/neon/lib/src/utils/dialog.dart new file mode 100644 index 00000000..35275f32 --- /dev/null +++ b/packages/neon/neon/lib/src/utils/dialog.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'package:neon/src/widgets/dialog.dart'; + +Future showConfirmationDialog(final BuildContext context, final String title) async => + await showDialog( + context: context, + builder: (final context) => NeonConfirmationDialog( + title: title, + ), + ) ?? + false; + +Future showRenameDialog({ + required final BuildContext context, + required final String title, + required final String value, + final Key? key, +}) async => + showDialog( + context: context, + builder: (final context) => RenameDialog( + title: title, + value: value, + key: key, + ), + ); diff --git a/packages/neon/neon/lib/src/utils/rename_dialog.dart b/packages/neon/neon/lib/src/utils/rename_dialog.dart deleted file mode 100644 index bef84826..00000000 --- a/packages/neon/neon/lib/src/utils/rename_dialog.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:neon/src/utils/validators.dart'; -import 'package:neon/src/widgets/dialog.dart'; - -Future showRenameDialog({ - required final BuildContext context, - required final String title, - required final String value, - final Key? key, -}) async => - showDialog( - context: context, - builder: (final context) => _RenameDialog( - title: title, - value: value, - key: key, - ), - ); - -class _RenameDialog extends StatefulWidget { - const _RenameDialog({ - required this.title, - required this.value, - super.key, - }); - - final String title; - final String value; - - @override - State<_RenameDialog> createState() => _RenameDialogState(); -} - -class _RenameDialogState extends State<_RenameDialog> { - final formKey = GlobalKey(); - - final controller = TextEditingController(); - - @override - void initState() { - controller.text = widget.value; - super.initState(); - } - - @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) => NeonDialog( - title: Text(widget.title), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TextFormField( - autofocus: true, - controller: controller, - validator: (final input) => validateNotEmpty(context, input), - onFieldSubmitted: (final _) { - submit(); - }, - ), - ElevatedButton( - onPressed: submit, - child: Text(widget.title), - ), - ], - ), - ), - ], - ); -} diff --git a/packages/neon/neon/lib/src/widgets/account_selection_dialog.dart b/packages/neon/neon/lib/src/widgets/account_selection_dialog.dart deleted file mode 100644 index 5c5a449a..00000000 --- a/packages/neon/neon/lib/src/widgets/account_selection_dialog.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:meta/meta.dart'; -import 'package:neon/src/blocs/accounts.dart'; -import 'package:neon/src/theme/dialog.dart'; -import 'package:neon/src/utils/provider.dart'; -import 'package:neon/src/widgets/account_tile.dart'; - -@internal -class NeonAccountSelectionDialog extends StatelessWidget { - const NeonAccountSelectionDialog({ - this.highlightActiveAccount = false, - this.children, - super.key, - }); - - final bool highlightActiveAccount; - final List? children; - - @override - Widget build(final BuildContext context) { - final accountsBloc = NeonProvider.of(context); - final accounts = accountsBloc.accounts.value; - final activeAccount = accountsBloc.activeAccount.value!; - - final sortedAccounts = List.of(accounts) - ..removeWhere((final account) => account.id == activeAccount.id) - ..insert(0, activeAccount); - - final tiles = sortedAccounts - .map( - (final account) => NeonAccountTile( - account: account, - trailing: highlightActiveAccount && account.id == activeAccount.id ? const Icon(Icons.check_circle) : null, - onTap: () { - Navigator.of(context).pop(account); - }, - ), - ) - .toList(); - if (highlightActiveAccount && accounts.length > 1) { - tiles.insert(1, const Divider()); - } - - final body = SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...tiles, - ...?children, - ], - ), - ); - - return Dialog( - child: IntrinsicHeight( - child: Container( - padding: const EdgeInsets.all(24), - constraints: NeonDialogTheme.of(context).constraints, - child: body, - ), - ), - ); - } -} diff --git a/packages/neon/neon/lib/src/widgets/account_switcher_button.dart b/packages/neon/neon/lib/src/widgets/account_switcher_button.dart index d021aab8..a919be15 100644 --- a/packages/neon/neon/lib/src/widgets/account_switcher_button.dart +++ b/packages/neon/neon/lib/src/widgets/account_switcher_button.dart @@ -6,8 +6,8 @@ import 'package:neon/src/models/account.dart'; import 'package:neon/src/pages/settings.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/provider.dart'; -import 'package:neon/src/widgets/account_selection_dialog.dart'; import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; +import 'package:neon/src/widgets/dialog.dart'; import 'package:neon/src/widgets/user_avatar.dart'; @internal diff --git a/packages/neon/neon/lib/src/widgets/dialog.dart b/packages/neon/neon/lib/src/widgets/dialog.dart index 1c6be14a..3fc96406 100644 --- a/packages/neon/neon/lib/src/widgets/dialog.dart +++ b/packages/neon/neon/lib/src/widgets/dialog.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; +import 'package:neon/blocs.dart'; +import 'package:neon/src/widgets/account_tile.dart'; +import 'package:neon/theme.dart'; +import 'package:neon/utils.dart'; /// A Neon material design dialog based on [SimpleDialog]. class NeonDialog extends StatelessWidget { @@ -31,3 +36,170 @@ class NeonDialog extends StatelessWidget { children: children, ); } + +class NeonConfirmationDialog extends StatelessWidget { + const NeonConfirmationDialog({ + required this.title, + super.key, + }); + + final String title; + + @override + Widget build(final BuildContext context) { + final confirm = ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: NcColors.accept, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(NeonLocalizations.of(context).actionYes), + ); + + final decline = ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: NcColors.decline, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(NeonLocalizations.of(context).actionNo), + ); + + return AlertDialog( + title: Text(title), + actionsAlignment: MainAxisAlignment.spaceEvenly, + actions: [ + decline, + confirm, + ], + ); + } +} + +class RenameDialog extends StatefulWidget { + const RenameDialog({ + required this.title, + required this.value, + super.key, + }); + + final String title; + final String value; + + @override + State createState() => _RenameDialogState(); +} + +class _RenameDialogState extends State { + final formKey = GlobalKey(); + + final controller = TextEditingController(); + + @override + void initState() { + controller.text = widget.value; + super.initState(); + } + + @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) => NeonDialog( + title: Text(widget.title), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextFormField( + autofocus: true, + controller: controller, + validator: (final input) => validateNotEmpty(context, input), + onFieldSubmitted: (final _) { + submit(); + }, + ), + ElevatedButton( + onPressed: submit, + child: Text(widget.title), + ), + ], + ), + ), + ], + ); +} + +@internal +class NeonAccountSelectionDialog extends StatelessWidget { + const NeonAccountSelectionDialog({ + this.highlightActiveAccount = false, + this.children, + super.key, + }); + + final bool highlightActiveAccount; + final List? children; + + @override + Widget build(final BuildContext context) { + final dialogTheme = NeonDialogTheme.of(context); + final accountsBloc = NeonProvider.of(context); + final accounts = accountsBloc.accounts.value; + final activeAccount = accountsBloc.activeAccount.value!; + + final sortedAccounts = List.of(accounts) + ..removeWhere((final account) => account.id == activeAccount.id) + ..insert(0, activeAccount); + + final tiles = sortedAccounts + .map( + (final account) => NeonAccountTile( + account: account, + trailing: highlightActiveAccount && account.id == activeAccount.id ? const Icon(Icons.check_circle) : null, + onTap: () { + Navigator.of(context).pop(account); + }, + ), + ) + .toList(); + if (highlightActiveAccount && accounts.length > 1) { + tiles.insert(1, const Divider()); + } + + final body = SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...tiles, + ...?children, + ], + ), + ); + + return Dialog( + child: IntrinsicHeight( + child: Container( + padding: const EdgeInsets.all(24), + constraints: dialogTheme.constraints, + child: body, + ), + ), + ); + } +} diff --git a/packages/neon/neon/lib/utils.dart b/packages/neon/neon/lib/utils.dart index f3d8012e..ea6f4c53 100644 --- a/packages/neon/neon/lib/utils.dart +++ b/packages/neon/neon/lib/utils.dart @@ -1,9 +1,8 @@ export 'package:neon/l10n/localizations.dart'; export 'package:neon/src/utils/app_route.dart'; -export 'package:neon/src/utils/confirmation_dialog.dart'; +export 'package:neon/src/utils/dialog.dart'; export 'package:neon/src/utils/exceptions.dart'; export 'package:neon/src/utils/hex_color.dart'; export 'package:neon/src/utils/provider.dart'; -export 'package:neon/src/utils/rename_dialog.dart'; export 'package:neon/src/utils/request_manager.dart' hide Cache; export 'package:neon/src/utils/validators.dart'; diff --git a/packages/neon/neon/lib/widgets.dart b/packages/neon/neon/lib/widgets.dart index a1b6d690..523fd2c7 100644 --- a/packages/neon/neon/lib/widgets.dart +++ b/packages/neon/neon/lib/widgets.dart @@ -1,4 +1,4 @@ -export 'package:neon/src/widgets/dialog.dart'; +export 'package:neon/src/widgets/dialog.dart' hide NeonAccountSelectionDialog; export 'package:neon/src/widgets/error.dart'; export 'package:neon/src/widgets/image.dart'; export 'package:neon/src/widgets/linear_progress_indicator.dart'; diff --git a/packages/neon/neon_files/lib/dialogs/choose_create.dart b/packages/neon/neon_files/lib/dialogs/choose_create.dart deleted file mode 100644 index 683fc80d..00000000 --- a/packages/neon/neon_files/lib/dialogs/choose_create.dart +++ /dev/null @@ -1,121 +0,0 @@ -part of '../neon_files.dart'; - -class FilesChooseCreateDialog extends StatefulWidget { - const FilesChooseCreateDialog({ - required this.bloc, - required this.basePath, - super.key, - }); - - final FilesBloc bloc; - final PathUri basePath; - - @override - State createState() => _FilesChooseCreateDialogState(); -} - -class _FilesChooseCreateDialogState extends State { - Future uploadFromPick(final FileType type) async { - final result = await FilePicker.platform.pickFiles( - allowMultiple: true, - type: type, - ); - if (result != null) { - for (final file in result.files) { - await upload(File(file.path!)); - } - } - } - - Future upload(final File file) async { - final sizeWarning = widget.bloc.options.uploadSizeWarning.value; - if (sizeWarning != null) { - final stat = file.statSync(); - if (stat.size > sizeWarning) { - if (!(await showConfirmationDialog( - context, - FilesLocalizations.of(context).uploadConfirmSizeWarning( - filesize(sizeWarning), - filesize(stat.size), - ), - ))) { - return; - } - } - } - widget.bloc.uploadFile( - widget.basePath.join(PathUri.parse(p.basename(file.path))), - file.path, - ); - } - - @override - Widget build(final BuildContext context) => NeonDialog( - children: [ - ListTile( - leading: Icon( - MdiIcons.filePlus, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(FilesLocalizations.of(context).uploadFiles), - onTap: () async { - await uploadFromPick(FileType.any); - - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ListTile( - leading: Icon( - MdiIcons.fileImagePlus, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(FilesLocalizations.of(context).uploadImages), - onTap: () async { - await uploadFromPick(FileType.image); - - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - if (NeonPlatform.instance.canUseCamera) ...[ - ListTile( - leading: Icon( - MdiIcons.cameraPlus, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(FilesLocalizations.of(context).uploadCamera), - onTap: () async { - Navigator.of(context).pop(); - - final picker = ImagePicker(); - final result = await picker.pickImage(source: ImageSource.camera); - if (result != null) { - await upload(File(result.path)); - } - }, - ), - ], - ListTile( - leading: Icon( - MdiIcons.folderPlus, - color: Theme.of(context).colorScheme.primary, - ), - title: Text(FilesLocalizations.of(context).folderCreate), - onTap: () async { - Navigator.of(context).pop(); - - final result = await showDialog( - context: context, - builder: (final context) => const FilesCreateFolderDialog(), - ); - if (result != null) { - widget.bloc.browser.createFolder(widget.basePath.join(PathUri.parse(result))); - } - }, - ), - ], - ); -} diff --git a/packages/neon/neon_files/lib/dialogs/choose_folder.dart b/packages/neon/neon_files/lib/dialogs/choose_folder.dart deleted file mode 100644 index d0b79cc9..00000000 --- a/packages/neon/neon_files/lib/dialogs/choose_folder.dart +++ /dev/null @@ -1,66 +0,0 @@ -part of '../neon_files.dart'; - -class FilesChooseFolderDialog extends StatelessWidget { - const FilesChooseFolderDialog({ - required this.bloc, - required this.filesBloc, - required this.originalPath, - super.key, - }); - - final FilesBrowserBloc bloc; - final FilesBloc filesBloc; - - final PathUri originalPath; - - @override - Widget build(final BuildContext context) => AlertDialog( - title: Text(FilesLocalizations.of(context).folderChoose), - contentPadding: EdgeInsets.zero, - content: SizedBox( - width: double.maxFinite, - child: Column( - children: [ - Expanded( - child: FilesBrowserView( - bloc: bloc, - filesBloc: filesBloc, - mode: FilesBrowserMode.selectDirectory, - ), - ), - StreamBuilder( - stream: bloc.uri, - builder: (final context, final uriSnapshot) => uriSnapshot.hasData - ? Container( - margin: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (final context) => const FilesCreateFolderDialog(), - ); - if (result != null) { - bloc.createFolder(uriSnapshot.requireData.join(PathUri.parse(result))); - } - }, - child: Text(FilesLocalizations.of(context).folderCreate), - ), - ElevatedButton( - onPressed: originalPath != uriSnapshot.requireData - ? () => Navigator.of(context).pop(uriSnapshot.requireData) - : null, - child: Text(FilesLocalizations.of(context).folderChoose), - ), - ], - ), - ) - : const SizedBox(), - ), - ], - ), - ), - ); -} diff --git a/packages/neon/neon_files/lib/dialogs/create_folder.dart b/packages/neon/neon_files/lib/dialogs/create_folder.dart deleted file mode 100644 index 3a00d968..00000000 --- a/packages/neon/neon_files/lib/dialogs/create_folder.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of '../neon_files.dart'; - -class FilesCreateFolderDialog extends StatefulWidget { - const FilesCreateFolderDialog({ - super.key, - }); - - @override - State createState() => _FilesCreateFolderDialogState(); -} - -class _FilesCreateFolderDialogState 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) => NeonDialog( - title: Text(FilesLocalizations.of(context).folderCreate), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TextFormField( - controller: controller, - decoration: InputDecoration( - hintText: FilesLocalizations.of(context).folderName, - ), - autofocus: true, - validator: (final input) => validateNotEmpty(context, input), - onFieldSubmitted: (final _) { - submit(); - }, - ), - ElevatedButton( - onPressed: submit, - child: Text(FilesLocalizations.of(context).folderCreate), - ), - ], - ), - ), - ], - ); -} diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index 31882f28..f2eda4d9 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -28,12 +28,10 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:file_icons/file_icons.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:neon/blocs.dart'; import 'package:neon/models.dart'; import 'package:neon/platform.dart'; @@ -44,6 +42,7 @@ import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/routes.dart'; +import 'package:neon_files/widgets/dialog.dart'; import 'package:neon_files/widgets/file_list_tile.dart'; import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; @@ -57,9 +56,6 @@ import 'package:universal_io/io.dart'; part 'blocs/browser.dart'; part 'blocs/files.dart'; -part 'dialogs/choose_create.dart'; -part 'dialogs/choose_folder.dart'; -part 'dialogs/create_folder.dart'; part 'models/file_details.dart'; part 'options.dart'; part 'pages/details.dart'; diff --git a/packages/neon/neon_files/lib/widgets/actions.dart b/packages/neon/neon_files/lib/widgets/actions.dart index bbe1656b..ec3de4ac 100644 --- a/packages/neon/neon_files/lib/widgets/actions.dart +++ b/packages/neon/neon_files/lib/widgets/actions.dart @@ -4,6 +4,7 @@ import 'package:neon/platform.dart'; import 'package:neon/utils.dart'; import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/neon_files.dart'; +import 'package:neon_files/widgets/dialog.dart'; import 'package:nextcloud/webdav.dart'; class FileActions extends StatelessWidget { diff --git a/packages/neon/neon_files/lib/widgets/dialog.dart b/packages/neon/neon_files/lib/widgets/dialog.dart new file mode 100644 index 00000000..5a003845 --- /dev/null +++ b/packages/neon/neon_files/lib/widgets/dialog.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:file_picker/file_picker.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:neon/platform.dart'; +import 'package:neon/utils.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_files/l10n/localizations.dart'; +import 'package:neon_files/neon_files.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:path/path.dart' as p; +import 'package:universal_io/io.dart'; + +class FilesChooseCreateDialog extends StatefulWidget { + const FilesChooseCreateDialog({ + required this.bloc, + required this.basePath, + super.key, + }); + + final FilesBloc bloc; + final PathUri basePath; + + @override + State createState() => _FilesChooseCreateDialogState(); +} + +class _FilesChooseCreateDialogState extends State { + Future uploadFromPick(final FileType type) async { + final result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: type, + ); + if (result != null) { + for (final file in result.files) { + await upload(File(file.path!)); + } + } + } + + Future upload(final File file) async { + final sizeWarning = widget.bloc.options.uploadSizeWarning.value; + if (sizeWarning != null) { + final stat = file.statSync(); + if (stat.size > sizeWarning) { + if (!(await showConfirmationDialog( + context, + FilesLocalizations.of(context).uploadConfirmSizeWarning( + filesize(sizeWarning), + filesize(stat.size), + ), + ))) { + return; + } + } + } + widget.bloc.uploadFile( + widget.basePath.join(PathUri.parse(p.basename(file.path))), + file.path, + ); + } + + @override + Widget build(final BuildContext context) => NeonDialog( + children: [ + ListTile( + leading: Icon( + MdiIcons.filePlus, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(FilesLocalizations.of(context).uploadFiles), + onTap: () async { + await uploadFromPick(FileType.any); + + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ListTile( + leading: Icon( + MdiIcons.fileImagePlus, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(FilesLocalizations.of(context).uploadImages), + onTap: () async { + await uploadFromPick(FileType.image); + + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + if (NeonPlatform.instance.canUseCamera) ...[ + ListTile( + leading: Icon( + MdiIcons.cameraPlus, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(FilesLocalizations.of(context).uploadCamera), + onTap: () async { + Navigator.of(context).pop(); + + final picker = ImagePicker(); + final result = await picker.pickImage(source: ImageSource.camera); + if (result != null) { + await upload(File(result.path)); + } + }, + ), + ], + ListTile( + leading: Icon( + MdiIcons.folderPlus, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(FilesLocalizations.of(context).folderCreate), + onTap: () async { + Navigator.of(context).pop(); + + final result = await showDialog( + context: context, + builder: (final context) => const FilesCreateFolderDialog(), + ); + if (result != null) { + widget.bloc.browser.createFolder(widget.basePath.join(PathUri.parse(result))); + } + }, + ), + ], + ); +} + +class FilesChooseFolderDialog extends StatelessWidget { + const FilesChooseFolderDialog({ + required this.bloc, + required this.filesBloc, + required this.originalPath, + super.key, + }); + + final FilesBrowserBloc bloc; + final FilesBloc filesBloc; + + final PathUri originalPath; + + @override + Widget build(final BuildContext context) => AlertDialog( + title: Text(FilesLocalizations.of(context).folderChoose), + contentPadding: EdgeInsets.zero, + content: SizedBox( + width: double.maxFinite, + child: Column( + children: [ + Expanded( + child: FilesBrowserView( + bloc: bloc, + filesBloc: filesBloc, + mode: FilesBrowserMode.selectDirectory, + ), + ), + StreamBuilder( + stream: bloc.uri, + builder: (final context, final uriSnapshot) => uriSnapshot.hasData + ? Container( + margin: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (final context) => const FilesCreateFolderDialog(), + ); + if (result != null) { + bloc.createFolder(uriSnapshot.requireData.join(PathUri.parse(result))); + } + }, + child: Text(FilesLocalizations.of(context).folderCreate), + ), + ElevatedButton( + onPressed: originalPath != uriSnapshot.requireData + ? () => Navigator.of(context).pop(uriSnapshot.requireData) + : null, + child: Text(FilesLocalizations.of(context).folderChoose), + ), + ], + ), + ) + : const SizedBox(), + ), + ], + ), + ), + ); +} + +class FilesCreateFolderDialog extends StatefulWidget { + const FilesCreateFolderDialog({ + super.key, + }); + + @override + State createState() => _FilesCreateFolderDialogState(); +} + +class _FilesCreateFolderDialogState 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) => NeonDialog( + title: Text(FilesLocalizations.of(context).folderCreate), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextFormField( + controller: controller, + decoration: InputDecoration( + hintText: FilesLocalizations.of(context).folderName, + ), + autofocus: true, + validator: (final input) => validateNotEmpty(context, input), + onFieldSubmitted: (final _) { + submit(); + }, + ), + ElevatedButton( + onPressed: submit, + child: Text(FilesLocalizations.of(context).folderCreate), + ), + ], + ), + ), + ], + ); +} diff --git a/packages/neon/neon_news/lib/dialogs/add_feed.dart b/packages/neon/neon_news/lib/dialogs/add_feed.dart deleted file mode 100644 index 4e99e8e1..00000000 --- a/packages/neon/neon_news/lib/dialogs/add_feed.dart +++ /dev/null @@ -1,108 +0,0 @@ -part of '../neon_news.dart'; - -class NewsAddFeedDialog extends StatefulWidget { - const NewsAddFeedDialog({ - required this.bloc, - this.folderID, - super.key, - }); - - final NewsBloc bloc; - 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) => ResultBuilder>.behaviorSubject( - subject: widget.bloc.folders, - builder: (final context, final folders) => NeonDialog( - title: Text(NewsLocalizations.of(context).feedAdd), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TextFormField( - autofocus: true, - controller: controller, - decoration: const InputDecoration( - hintText: 'https://...', - ), - keyboardType: TextInputType.url, - validator: (final input) => validateHttpUrl(context, input), - onFieldSubmitted: (final _) { - submit(); - }, - ), - if (widget.folderID == null) ...[ - Center( - child: NeonError( - folders.error, - onRetry: widget.bloc.refresh, - ), - ), - Center( - child: NeonLinearProgressIndicator( - visible: folders.isLoading, - ), - ), - if (folders.hasData) ...[ - NewsFolderSelect( - folders: folders.requireData, - value: folder, - onChanged: (final f) { - setState(() { - folder = f; - }); - }, - ), - ], - ], - ElevatedButton( - onPressed: folders.hasData ? submit : null, - child: Text(NewsLocalizations.of(context).feedAdd), - ), - ], - ), - ), - ], - ), - ); -} diff --git a/packages/neon/neon_news/lib/dialogs/create_folder.dart b/packages/neon/neon_news/lib/dialogs/create_folder.dart deleted file mode 100644 index 46890369..00000000 --- a/packages/neon/neon_news/lib/dialogs/create_folder.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of '../neon_news.dart'; - -class NewsCreateFolderDialog extends StatefulWidget { - 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) => NeonDialog( - title: Text(NewsLocalizations.of(context).folderCreate), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TextFormField( - autofocus: true, - controller: controller, - decoration: InputDecoration( - hintText: NewsLocalizations.of(context).folderCreateName, - ), - validator: (final input) => validateNotEmpty(context, input), - onFieldSubmitted: (final _) { - submit(); - }, - ), - ElevatedButton( - onPressed: submit, - child: Text(NewsLocalizations.of(context).folderCreate), - ), - ], - ), - ), - ], - ); -} diff --git a/packages/neon/neon_news/lib/dialogs/feed_show_url.dart b/packages/neon/neon_news/lib/dialogs/feed_show_url.dart deleted file mode 100644 index ba6be6ca..00000000 --- a/packages/neon/neon_news/lib/dialogs/feed_show_url.dart +++ /dev/null @@ -1,46 +0,0 @@ -part of '../neon_news.dart'; - -class NewsFeedShowURLDialog extends StatefulWidget { - const NewsFeedShowURLDialog({ - required this.feed, - super.key, - }); - - final news.Feed feed; - - @override - State createState() => _NewsFeedShowURLDialogState(); -} - -class _NewsFeedShowURLDialogState extends State { - @override - Widget build(final BuildContext context) => AlertDialog( - title: Text(widget.feed.url), - actions: [ - ElevatedButton( - onPressed: () async { - await Clipboard.setData( - ClipboardData( - text: widget.feed.url, - ), - ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(NewsLocalizations.of(context).feedCopiedURL), - ), - ); - Navigator.of(context).pop(); - } - }, - child: Text(NewsLocalizations.of(context).feedCopyURL), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(NewsLocalizations.of(context).actionClose), - ), - ], - ); -} diff --git a/packages/neon/neon_news/lib/dialogs/feed_update_error.dart b/packages/neon/neon_news/lib/dialogs/feed_update_error.dart deleted file mode 100644 index c74f14ec..00000000 --- a/packages/neon/neon_news/lib/dialogs/feed_update_error.dart +++ /dev/null @@ -1,46 +0,0 @@ -part of '../neon_news.dart'; - -class NewsFeedUpdateErrorDialog extends StatefulWidget { - const NewsFeedUpdateErrorDialog({ - required this.feed, - super.key, - }); - - final news.Feed feed; - - @override - State createState() => _NewsFeedUpdateErrorDialogState(); -} - -class _NewsFeedUpdateErrorDialogState extends State { - @override - Widget build(final BuildContext context) => AlertDialog( - title: Text(widget.feed.lastUpdateError!), - actions: [ - ElevatedButton( - onPressed: () async { - await Clipboard.setData( - ClipboardData( - text: widget.feed.lastUpdateError!, - ), - ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(NewsLocalizations.of(context).feedCopiedErrorMessage), - ), - ); - Navigator.of(context).pop(); - } - }, - child: Text(NewsLocalizations.of(context).feedCopyErrorMessage), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(NewsLocalizations.of(context).actionClose), - ), - ], - ); -} diff --git a/packages/neon/neon_news/lib/dialogs/move_feed.dart b/packages/neon/neon_news/lib/dialogs/move_feed.dart deleted file mode 100644 index 2a84d6bf..00000000 --- a/packages/neon/neon_news/lib/dialogs/move_feed.dart +++ /dev/null @@ -1,57 +0,0 @@ -part of '../neon_news.dart'; - -class NewsMoveFeedDialog extends StatefulWidget { - const NewsMoveFeedDialog({ - required this.folders, - required this.feed, - super.key, - }); - - final List folders; - 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 - Widget build(final BuildContext context) => NeonDialog( - title: Text(NewsLocalizations.of(context).feedMove), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - NewsFolderSelect( - folders: widget.folders, - value: widget.feed.folderId != null - ? widget.folders.singleWhere((final folder) => folder.id == widget.feed.folderId) - : null, - onChanged: (final f) { - setState(() { - folder = f; - }); - }, - ), - ElevatedButton( - onPressed: submit, - child: Text(NewsLocalizations.of(context).feedMove), - ), - ], - ), - ), - ], - ); -} diff --git a/packages/neon/neon_news/lib/neon_news.dart b/packages/neon/neon_news/lib/neon_news.dart index 53bb272a..8b292e6b 100644 --- a/packages/neon/neon_news/lib/neon_news.dart +++ b/packages/neon/neon_news/lib/neon_news.dart @@ -28,7 +28,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:go_router/go_router.dart'; @@ -44,6 +43,7 @@ import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_news/l10n/localizations.dart'; import 'package:neon_news/routes.dart'; +import 'package:neon_news/widgets/dialog.dart'; import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/news.dart' as news; import 'package:nextcloud/nextcloud.dart'; @@ -56,11 +56,6 @@ import 'package:webview_flutter/webview_flutter.dart'; part 'blocs/article.dart'; part 'blocs/articles.dart'; part 'blocs/news.dart'; -part 'dialogs/add_feed.dart'; -part 'dialogs/create_folder.dart'; -part 'dialogs/feed_show_url.dart'; -part 'dialogs/feed_update_error.dart'; -part 'dialogs/move_feed.dart'; part 'options.dart'; part 'pages/article.dart'; part 'pages/feed.dart'; diff --git a/packages/neon/neon_news/lib/widgets/dialog.dart b/packages/neon/neon_news/lib/widgets/dialog.dart new file mode 100644 index 00000000..b1cdce0a --- /dev/null +++ b/packages/neon/neon_news/lib/widgets/dialog.dart @@ -0,0 +1,320 @@ +import 'dart:async'; + +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; + +class NewsAddFeedDialog extends StatefulWidget { + const NewsAddFeedDialog({ + required this.bloc, + this.folderID, + super.key, + }); + + final NewsBloc bloc; + 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) => ResultBuilder>.behaviorSubject( + subject: widget.bloc.folders, + builder: (final context, final folders) => NeonDialog( + title: Text(NewsLocalizations.of(context).feedAdd), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextFormField( + autofocus: true, + controller: controller, + decoration: const InputDecoration( + hintText: 'https://...', + ), + keyboardType: TextInputType.url, + validator: (final input) => validateHttpUrl(context, input), + onFieldSubmitted: (final _) { + submit(); + }, + ), + if (widget.folderID == null) ...[ + Center( + child: NeonError( + folders.error, + onRetry: widget.bloc.refresh, + ), + ), + Center( + child: NeonLinearProgressIndicator( + visible: folders.isLoading, + ), + ), + if (folders.hasData) ...[ + NewsFolderSelect( + folders: folders.requireData, + value: folder, + onChanged: (final f) { + setState(() { + folder = f; + }); + }, + ), + ], + ], + ElevatedButton( + onPressed: folders.hasData ? submit : null, + child: Text(NewsLocalizations.of(context).feedAdd), + ), + ], + ), + ), + ], + ), + ); +} + +class NewsCreateFolderDialog extends StatefulWidget { + 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) => NeonDialog( + title: Text(NewsLocalizations.of(context).folderCreate), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextFormField( + autofocus: true, + controller: controller, + decoration: InputDecoration( + hintText: NewsLocalizations.of(context).folderCreateName, + ), + validator: (final input) => validateNotEmpty(context, input), + onFieldSubmitted: (final _) { + submit(); + }, + ), + ElevatedButton( + onPressed: submit, + child: Text(NewsLocalizations.of(context).folderCreate), + ), + ], + ), + ), + ], + ); +} + +class NewsFeedShowURLDialog extends StatefulWidget { + const NewsFeedShowURLDialog({ + required this.feed, + super.key, + }); + + final news.Feed feed; + + @override + State createState() => _NewsFeedShowURLDialogState(); +} + +class _NewsFeedShowURLDialogState extends State { + @override + Widget build(final BuildContext context) => AlertDialog( + title: Text(widget.feed.url), + actions: [ + ElevatedButton( + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: widget.feed.url, + ), + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(NewsLocalizations.of(context).feedCopiedURL), + ), + ); + Navigator.of(context).pop(); + } + }, + child: Text(NewsLocalizations.of(context).feedCopyURL), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(NewsLocalizations.of(context).actionClose), + ), + ], + ); +} + +class NewsFeedUpdateErrorDialog extends StatefulWidget { + const NewsFeedUpdateErrorDialog({ + required this.feed, + super.key, + }); + + final news.Feed feed; + + @override + State createState() => _NewsFeedUpdateErrorDialogState(); +} + +class _NewsFeedUpdateErrorDialogState extends State { + @override + Widget build(final BuildContext context) => AlertDialog( + title: Text(widget.feed.lastUpdateError!), + actions: [ + ElevatedButton( + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: widget.feed.lastUpdateError!, + ), + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(NewsLocalizations.of(context).feedCopiedErrorMessage), + ), + ); + Navigator.of(context).pop(); + } + }, + child: Text(NewsLocalizations.of(context).feedCopyErrorMessage), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(NewsLocalizations.of(context).actionClose), + ), + ], + ); +} + +class NewsMoveFeedDialog extends StatefulWidget { + const NewsMoveFeedDialog({ + required this.folders, + required this.feed, + super.key, + }); + + final List folders; + 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 + Widget build(final BuildContext context) => NeonDialog( + title: Text(NewsLocalizations.of(context).feedMove), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + NewsFolderSelect( + folders: widget.folders, + value: widget.feed.folderId != null + ? widget.folders.singleWhere((final folder) => folder.id == widget.feed.folderId) + : null, + onChanged: (final f) { + setState(() { + folder = f; + }); + }, + ), + ElevatedButton( + onPressed: submit, + child: Text(NewsLocalizations.of(context).feedMove), + ), + ], + ), + ), + ], + ); +} diff --git a/packages/neon/neon_notes/lib/dialogs/select_category.dart b/packages/neon/neon_notes/lib/dialogs/select_category.dart deleted file mode 100644 index 04b4b275..00000000 --- a/packages/neon/neon_notes/lib/dialogs/select_category.dart +++ /dev/null @@ -1,70 +0,0 @@ -part of '../neon_notes.dart'; - -class NotesSelectCategoryDialog extends StatefulWidget { - const NotesSelectCategoryDialog({ - required this.bloc, - this.initialCategory, - super.key, - }); - - final NotesBloc bloc; - final String? initialCategory; - - @override - State createState() => _NotesSelectCategoryDialogState(); -} - -class _NotesSelectCategoryDialogState extends State { - final formKey = GlobalKey(); - - String? selectedCategory; - - void submit() { - if (formKey.currentState!.validate()) { - Navigator.of(context).pop(selectedCategory); - } - } - - @override - Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( - subject: widget.bloc.notesList, - builder: (final context, final notes) => NeonDialog( - title: Text(NotesLocalizations.of(context).category), - children: [ - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Center( - child: NeonError( - notes.error, - onRetry: widget.bloc.refresh, - ), - ), - Center( - child: NeonLinearProgressIndicator( - visible: notes.isLoading, - ), - ), - if (notes.hasData) ...[ - NotesCategorySelect( - categories: notes.requireData.map((final note) => note.category).toSet().toList(), - initialValue: widget.initialCategory, - onChanged: (final category) { - selectedCategory = category; - }, - onSubmitted: submit, - ), - ], - ElevatedButton( - onPressed: submit, - child: Text(NotesLocalizations.of(context).noteSetCategory), - ), - ], - ), - ), - ], - ), - ); -} diff --git a/packages/neon/neon_notes/lib/neon_notes.dart b/packages/neon/neon_notes/lib/neon_notes.dart index 4ca927e8..c1e1fcf2 100644 --- a/packages/neon/neon_notes/lib/neon_notes.dart +++ b/packages/neon/neon_notes/lib/neon_notes.dart @@ -42,6 +42,7 @@ import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_notes/l10n/localizations.dart'; import 'package:neon_notes/routes.dart'; +import 'package:neon_notes/widgets/dialog.dart'; import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/notes.dart' as notes; @@ -52,8 +53,6 @@ import 'package:wakelock_plus/wakelock_plus.dart'; part 'blocs/note.dart'; part 'blocs/notes.dart'; -part 'dialogs/create_note.dart'; -part 'dialogs/select_category.dart'; part 'options.dart'; part 'pages/category.dart'; part 'pages/main.dart'; diff --git a/packages/neon/neon_notes/lib/dialogs/create_note.dart b/packages/neon/neon_notes/lib/widgets/dialog.dart similarity index 53% rename from packages/neon/neon_notes/lib/dialogs/create_note.dart rename to packages/neon/neon_notes/lib/widgets/dialog.dart index ea521f22..90de24c4 100644 --- a/packages/neon/neon_notes/lib/dialogs/create_note.dart +++ b/packages/neon/neon_notes/lib/widgets/dialog.dart @@ -1,4 +1,10 @@ -part of '../neon_notes.dart'; +import 'package:flutter/material.dart'; +import 'package:neon/blocs.dart'; +import 'package:neon/utils.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_notes/l10n/localizations.dart'; +import 'package:neon_notes/neon_notes.dart'; +import 'package:nextcloud/notes.dart' as notes; class NotesCreateNoteDialog extends StatefulWidget { const NotesCreateNoteDialog({ @@ -86,3 +92,72 @@ class _NotesCreateNoteDialogState extends State { ), ); } + +class NotesSelectCategoryDialog extends StatefulWidget { + const NotesSelectCategoryDialog({ + required this.bloc, + this.initialCategory, + super.key, + }); + + final NotesBloc bloc; + final String? initialCategory; + + @override + State createState() => _NotesSelectCategoryDialogState(); +} + +class _NotesSelectCategoryDialogState extends State { + final formKey = GlobalKey(); + + String? selectedCategory; + + void submit() { + if (formKey.currentState!.validate()) { + Navigator.of(context).pop(selectedCategory); + } + } + + @override + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( + subject: widget.bloc.notesList, + builder: (final context, final notes) => NeonDialog( + title: Text(NotesLocalizations.of(context).category), + children: [ + Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Center( + child: NeonError( + notes.error, + onRetry: widget.bloc.refresh, + ), + ), + Center( + child: NeonLinearProgressIndicator( + visible: notes.isLoading, + ), + ), + if (notes.hasData) ...[ + NotesCategorySelect( + categories: notes.requireData.map((final note) => note.category).toSet().toList(), + initialValue: widget.initialCategory, + onChanged: (final category) { + selectedCategory = category; + }, + onSubmitted: submit, + ), + ], + ElevatedButton( + onPressed: submit, + child: Text(NotesLocalizations.of(context).noteSetCategory), + ), + ], + ), + ), + ], + ), + ); +}