Browse Source

refactor(neon,neon_files,neon_news,neon_notes): refactor dialog structure

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/998/head
Nikolas Rimikis 1 year ago
parent
commit
69ef5b2884
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 2
      packages/neon/neon/lib/src/pages/account_settings.dart
  2. 2
      packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart
  3. 2
      packages/neon/neon/lib/src/pages/settings.dart
  4. 27
      packages/neon/neon/lib/src/utils/dialog.dart
  5. 83
      packages/neon/neon/lib/src/utils/rename_dialog.dart
  6. 64
      packages/neon/neon/lib/src/widgets/account_selection_dialog.dart
  7. 2
      packages/neon/neon/lib/src/widgets/account_switcher_button.dart
  8. 172
      packages/neon/neon/lib/src/widgets/dialog.dart
  9. 3
      packages/neon/neon/lib/utils.dart
  10. 2
      packages/neon/neon/lib/widgets.dart
  11. 121
      packages/neon/neon_files/lib/dialogs/choose_create.dart
  12. 66
      packages/neon/neon_files/lib/dialogs/choose_folder.dart
  13. 58
      packages/neon/neon_files/lib/dialogs/create_folder.dart
  14. 6
      packages/neon/neon_files/lib/neon_files.dart
  15. 1
      packages/neon/neon_files/lib/widgets/actions.dart
  16. 257
      packages/neon/neon_files/lib/widgets/dialog.dart
  17. 108
      packages/neon/neon_news/lib/dialogs/add_feed.dart
  18. 58
      packages/neon/neon_news/lib/dialogs/create_folder.dart
  19. 46
      packages/neon/neon_news/lib/dialogs/feed_show_url.dart
  20. 46
      packages/neon/neon_news/lib/dialogs/feed_update_error.dart
  21. 57
      packages/neon/neon_news/lib/dialogs/move_feed.dart
  22. 7
      packages/neon/neon_news/lib/neon_news.dart
  23. 320
      packages/neon/neon_news/lib/widgets/dialog.dart
  24. 70
      packages/neon/neon_notes/lib/dialogs/select_category.dart
  25. 3
      packages/neon/neon_notes/lib/neon_notes.dart
  26. 77
      packages/neon/neon_notes/lib/widgets/dialog.dart

2
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;

2
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 {

2
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';

27
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<bool> showConfirmationDialog(final BuildContext context, final String title) async =>
await showDialog<bool>(
context: context,
builder: (final context) => NeonConfirmationDialog(
title: title,
),
) ??
false;
Future<String?> showRenameDialog({
required final BuildContext context,
required final String title,
required final String value,
final Key? key,
}) async =>
showDialog<String?>(
context: context,
builder: (final context) => RenameDialog(
title: title,
value: value,
key: key,
),
);

83
packages/neon/neon/lib/src/utils/rename_dialog.dart

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import 'package:neon/src/utils/validators.dart';
import 'package:neon/src/widgets/dialog.dart';
Future<String?> showRenameDialog({
required final BuildContext context,
required final String title,
required final String value,
final Key? key,
}) async =>
showDialog<String?>(
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<FormState>();
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),
),
],
),
),
],
);
}

64
packages/neon/neon/lib/src/widgets/account_selection_dialog.dart

@ -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<Widget>? children;
@override
Widget build(final BuildContext context) {
final accountsBloc = NeonProvider.of<AccountsBloc>(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<Widget>(
(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,
),
),
);
}
}

2
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

172
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<RenameDialog> createState() => _RenameDialogState();
}
class _RenameDialogState extends State<RenameDialog> {
final formKey = GlobalKey<FormState>();
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<Widget>? children;
@override
Widget build(final BuildContext context) {
final dialogTheme = NeonDialogTheme.of(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(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<Widget>(
(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,
),
),
);
}
}

3
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';

2
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';

121
packages/neon/neon_files/lib/dialogs/choose_create.dart

@ -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<FilesChooseCreateDialog> createState() => _FilesChooseCreateDialogState();
}
class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
Future<void> 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<void> 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<String>(
context: context,
builder: (final context) => const FilesCreateFolderDialog(),
);
if (result != null) {
widget.bloc.browser.createFolder(widget.basePath.join(PathUri.parse(result)));
}
},
),
],
);
}

66
packages/neon/neon_files/lib/dialogs/choose_folder.dart

@ -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<PathUri>(
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<String>(
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(),
),
],
),
),
);
}

58
packages/neon/neon_files/lib/dialogs/create_folder.dart

@ -1,58 +0,0 @@
part of '../neon_files.dart';
class FilesCreateFolderDialog extends StatefulWidget {
const FilesCreateFolderDialog({
super.key,
});
@override
State<FilesCreateFolderDialog> createState() => _FilesCreateFolderDialogState();
}
class _FilesCreateFolderDialogState extends State<FilesCreateFolderDialog> {
final formKey = GlobalKey<FormState>();
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),
),
],
),
),
],
);
}

6
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';

1
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 {

257
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<FilesChooseCreateDialog> createState() => _FilesChooseCreateDialogState();
}
class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
Future<void> 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<void> 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<String>(
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<PathUri>(
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<String>(
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<FilesCreateFolderDialog> createState() => _FilesCreateFolderDialogState();
}
class _FilesCreateFolderDialogState extends State<FilesCreateFolderDialog> {
final formKey = GlobalKey<FormState>();
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),
),
],
),
),
],
);
}

108
packages/neon/neon_news/lib/dialogs/add_feed.dart

@ -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<NewsAddFeedDialog> createState() => _NewsAddFeedDialogState();
}
class _NewsAddFeedDialogState extends State<NewsAddFeedDialog> {
final formKey = GlobalKey<FormState>();
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<List<news.Folder>>.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),
),
],
),
),
],
),
);
}

58
packages/neon/neon_news/lib/dialogs/create_folder.dart

@ -1,58 +0,0 @@
part of '../neon_news.dart';
class NewsCreateFolderDialog extends StatefulWidget {
const NewsCreateFolderDialog({
super.key,
});
@override
State<NewsCreateFolderDialog> createState() => _NewsCreateFolderDialogState();
}
class _NewsCreateFolderDialogState extends State<NewsCreateFolderDialog> {
final formKey = GlobalKey<FormState>();
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),
),
],
),
),
],
);
}

46
packages/neon/neon_news/lib/dialogs/feed_show_url.dart

@ -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<NewsFeedShowURLDialog> createState() => _NewsFeedShowURLDialogState();
}
class _NewsFeedShowURLDialogState extends State<NewsFeedShowURLDialog> {
@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),
),
],
);
}

46
packages/neon/neon_news/lib/dialogs/feed_update_error.dart

@ -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<NewsFeedUpdateErrorDialog> createState() => _NewsFeedUpdateErrorDialogState();
}
class _NewsFeedUpdateErrorDialogState extends State<NewsFeedUpdateErrorDialog> {
@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),
),
],
);
}

57
packages/neon/neon_news/lib/dialogs/move_feed.dart

@ -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<news.Folder> folders;
final news.Feed feed;
@override
State<NewsMoveFeedDialog> createState() => _NewsMoveFeedDialogState();
}
class _NewsMoveFeedDialogState extends State<NewsMoveFeedDialog> {
final formKey = GlobalKey<FormState>();
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),
),
],
),
),
],
);
}

7
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';

320
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<NewsAddFeedDialog> createState() => _NewsAddFeedDialogState();
}
class _NewsAddFeedDialogState extends State<NewsAddFeedDialog> {
final formKey = GlobalKey<FormState>();
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<List<news.Folder>>.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<NewsCreateFolderDialog> createState() => _NewsCreateFolderDialogState();
}
class _NewsCreateFolderDialogState extends State<NewsCreateFolderDialog> {
final formKey = GlobalKey<FormState>();
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<NewsFeedShowURLDialog> createState() => _NewsFeedShowURLDialogState();
}
class _NewsFeedShowURLDialogState extends State<NewsFeedShowURLDialog> {
@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<NewsFeedUpdateErrorDialog> createState() => _NewsFeedUpdateErrorDialogState();
}
class _NewsFeedUpdateErrorDialogState extends State<NewsFeedUpdateErrorDialog> {
@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<news.Folder> folders;
final news.Feed feed;
@override
State<NewsMoveFeedDialog> createState() => _NewsMoveFeedDialogState();
}
class _NewsMoveFeedDialogState extends State<NewsMoveFeedDialog> {
final formKey = GlobalKey<FormState>();
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),
),
],
),
),
],
);
}

70
packages/neon/neon_notes/lib/dialogs/select_category.dart

@ -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<NotesSelectCategoryDialog> createState() => _NotesSelectCategoryDialogState();
}
class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
final formKey = GlobalKey<FormState>();
String? selectedCategory;
void submit() {
if (formKey.currentState!.validate()) {
Navigator.of(context).pop(selectedCategory);
}
}
@override
Widget build(final BuildContext context) => ResultBuilder<List<notes.Note>>.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),
),
],
),
),
],
),
);
}

3
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';

77
packages/neon/neon_notes/lib/dialogs/create_note.dart → 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<NotesCreateNoteDialog> {
),
);
}
class NotesSelectCategoryDialog extends StatefulWidget {
const NotesSelectCategoryDialog({
required this.bloc,
this.initialCategory,
super.key,
});
final NotesBloc bloc;
final String? initialCategory;
@override
State<NotesSelectCategoryDialog> createState() => _NotesSelectCategoryDialogState();
}
class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
final formKey = GlobalKey<FormState>();
String? selectedCategory;
void submit() {
if (formKey.currentState!.validate()) {
Navigator.of(context).pop(selectedCategory);
}
}
@override
Widget build(final BuildContext context) => ResultBuilder<List<notes.Note>>.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),
),
],
),
),
],
),
);
}
Loading…
Cancel
Save