diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 5c33e844..eee88018 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -438,161 +438,157 @@ class _HomePageState extends State { ), ); - return Scaffold( - resizeToAvoidBottomInset: false, - body: Row( - children: [ - if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ - drawer, - ], - Expanded( - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - drawer: navigationMode == NavigationMode.drawer ? drawer : null, - appBar: AppBar( - scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, - automaticallyImplyLeading: navigationMode == NavigationMode.drawer, - leadingWidth: isQuickBar ? kQuickBarWidth : null, - leading: isQuickBar - ? Container( - padding: const EdgeInsets.all(5), - child: capabilities.data?.capabilities.theming?.logo != null - ? NeonCachedUrlImage( - url: capabilities.data!.capabilities.theming!.logo!, - svgColor: Theme.of(context).iconTheme.color, - ) - : null, - ) - : null, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appImplementations.data! - .find(activeAppIDSnapshot.data!)! - .name(context), - ), - ), - ], - if (appImplementations.error != null) ...[ - const SizedBox( - width: 8, - ), - NeonException( - appImplementations.error, - onRetry: _appsBloc.refresh, - onlyIcon: true, - ), - ], - if (appImplementations.loading) ...[ - const SizedBox( - width: 8, + return Row( + children: [ + if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ + drawer, + ], + Expanded( + child: Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: false, + drawer: navigationMode == NavigationMode.drawer ? drawer : null, + appBar: AppBar( + scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, + automaticallyImplyLeading: navigationMode == NavigationMode.drawer, + leadingWidth: isQuickBar ? kQuickBarWidth : null, + leading: isQuickBar + ? Container( + padding: const EdgeInsets.all(5), + child: capabilities.data?.capabilities.theming?.logo != null + ? NeonCachedUrlImage( + url: capabilities.data!.capabilities.theming!.logo!, + svgColor: Theme.of(context).iconTheme.color, + ) + : null, + ) + : null, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appImplementations.data! + .find(activeAppIDSnapshot.data!)! + .name(context), ), - Expanded( - child: NeonLinearProgressIndicator( - color: Theme.of(context).appBarTheme.foregroundColor, - ), + ), + ], + if (appImplementations.error != null) ...[ + const SizedBox( + width: 8, + ), + NeonException( + appImplementations.error, + onRetry: _appsBloc.refresh, + onlyIcon: true, + ), + ], + if (appImplementations.loading) ...[ + const SizedBox( + width: 8, + ), + Expanded( + child: NeonLinearProgressIndicator( + color: Theme.of(context).appBarTheme.foregroundColor, ), - ], + ), ], - ), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall, - ), ], - ], - ), - actions: [ - if (notificationsAppImplementation.data != null) ...[ - StreamBuilder( - stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - return IconButton( - key: Key('app-${notificationsAppImplementation.data!.id}'), - icon: NeonAppImplementationIcon( - appImplementation: notificationsAppImplementation.data!, - unreadCount: unreadCount, - color: unreadCount > 0 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, - size: const Size.square(kAvatarSize * 2 / 3), - ), - onPressed: () async { - await _openNotifications( - notificationsAppImplementation.data!, - accounts, - account, - ); - }, - ); - }, + ), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall, ), ], - IconButton( - icon: IntrinsicWidth( - child: NeonAccountAvatar( - account: account, - ), - ), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSettingsPage( - bloc: _accountsBloc, - account: account, - ), + ], + ), + actions: [ + if (notificationsAppImplementation.data != null) ...[ + StreamBuilder( + stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + return IconButton( + key: Key('app-${notificationsAppImplementation.data!.id}'), + icon: NeonAppImplementationIcon( + appImplementation: notificationsAppImplementation.data!, + unreadCount: unreadCount, + color: unreadCount > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onBackground, + size: const Size.square(kAvatarSize * 2 / 3), ), + onPressed: () async { + await _openNotifications( + notificationsAppImplementation.data!, + accounts, + account, + ); + }, ); }, ), ], - ), - body: Row( - children: [ - if (navigationMode == NavigationMode.quickBar) ...[ - drawer, - ], - Expanded( - child: Column( - children: [ - if (appImplementations.data != null) ...[ - if (appImplementations.data!.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context) - .errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), + IconButton( + icon: IntrinsicWidth( + child: NeonAccountAvatar( + account: account, + ), + ), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => AccountSettingsPage( + bloc: _accountsBloc, + account: account, + ), + ), + ); + }, + ), + ], + ), + body: Row( + children: [ + if (navigationMode == NavigationMode.quickBar) ...[ + drawer, + ], + Expanded( + child: Column( + children: [ + if (appImplementations.data != null) ...[ + if (appImplementations.data!.isEmpty) ...[ + Expanded( + child: Center( + child: Text( + AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, ), ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ - Expanded( - child: appImplementations.data! - .find(activeAppIDSnapshot.data!)! - .buildPage(context, _appsBloc), - ), - ], + ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appImplementations.data! + .find(activeAppIDSnapshot.data!)! + .buildPage(context, _appsBloc), + ), ], ], ], - ), + ], ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ); } return Container(); diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index cb76d6ee..4fd6a164 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -30,12 +30,9 @@ class AppRouter extends RouterDelegate with ChangeNotifier, PopNavigato ] else ...[ MaterialPage( name: 'home', - child: Scaffold( - resizeToAvoidBottomInset: false, - body: HomePage( - key: Key(currentConfiguration!.id), - account: currentConfiguration!, - ), + child: HomePage( + key: Key(currentConfiguration!.id), + account: currentConfiguration!, ), ), ], diff --git a/packages/neon/neon_files/lib/dialogs/choose_folder.dart b/packages/neon/neon_files/lib/dialogs/choose_folder.dart index 6a7f9351..a51b495c 100644 --- a/packages/neon/neon_files/lib/dialogs/choose_folder.dart +++ b/packages/neon/neon_files/lib/dialogs/choose_folder.dart @@ -26,7 +26,6 @@ class FilesChooseFolderDialog extends StatelessWidget { bloc: bloc, filesBloc: filesBloc, enableFileActions: false, - enableCreateActions: false, onlyShowDirectories: true, ), ), diff --git a/packages/neon/neon_files/lib/pages/main.dart b/packages/neon/neon_files/lib/pages/main.dart index e3154542..a14b241f 100644 --- a/packages/neon/neon_files/lib/pages/main.dart +++ b/packages/neon/neon_files/lib/pages/main.dart @@ -23,25 +23,39 @@ class _FilesMainPageState extends State { } @override - Widget build(final BuildContext context) => FilesBrowserView( - bloc: widget.bloc.browser, - filesBloc: widget.bloc, - onPickFile: (final details) async { - final sizeWarning = widget.bloc.options.downloadSizeWarning.value; - if (sizeWarning != null && details.size > sizeWarning) { - // ignore: use_build_context_synchronously - if (!(await showConfirmationDialog( - context, + Widget build(final BuildContext context) => Scaffold( + body: FilesBrowserView( + bloc: widget.bloc.browser, + filesBloc: widget.bloc, + onPickFile: (final details) async { + final sizeWarning = widget.bloc.options.downloadSizeWarning.value; + if (sizeWarning != null && details.size > sizeWarning) { // ignore: use_build_context_synchronously - AppLocalizations.of(context).filesConfirmDownloadSizeWarning( - filesize(sizeWarning), - filesize(details.size), - ), - ))) { - return; + if (!(await showConfirmationDialog( + context, + // ignore: use_build_context_synchronously + AppLocalizations.of(context).filesConfirmDownloadSizeWarning( + filesize(sizeWarning), + filesize(details.size), + ), + ))) { + return; + } } - } - widget.bloc.openFile(details.path, details.etag!, details.mimeType); - }, + widget.bloc.openFile(details.path, details.etag!, details.mimeType); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await showDialog( + context: context, + builder: (final context) => FilesChooseCreateDialog( + bloc: widget.bloc, + basePath: widget.bloc.browser.path.value, + ), + ); + }, + child: const Icon(Icons.add), + ), ); } diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 78b295dc..a47dca51 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -6,7 +6,6 @@ class FilesBrowserView extends StatefulWidget { required this.filesBloc, this.onPickFile, this.enableFileActions = true, - this.enableCreateActions = true, this.onlyShowDirectories = false, super.key, // ignore: prefer_asserts_with_message @@ -16,7 +15,6 @@ class FilesBrowserView extends StatefulWidget { final FilesBloc filesBloc; final Function(FileDetails)? onPickFile; final bool enableFileActions; - final bool enableCreateActions; final bool onlyShowDirectories; @override @@ -55,149 +53,132 @@ class _FilesBrowserViewState extends State { } return false; }, - child: Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: widget.enableCreateActions - ? FloatingActionButton( - onPressed: () async { - await showDialog( - context: context, - builder: (final context) => FilesChooseCreateDialog( - bloc: widget.filesBloc, - basePath: widget.bloc.path.value, - ), - ); - }, - child: const Icon(Icons.add), - ) - : null, - body: SortBoxBuilder( - sortBox: filesSortBox, - sortPropertyOption: widget.bloc.options.filesSortPropertyOption, - sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, - input: files.data, - builder: (final context, final sorted) => NeonListView( - scrollKey: 'files-${pathSnapshot.data!.join('/')}', - withFloatingActionButton: true, - items: [ - for (final uploadTask in sorted == null - ? [] - : uploadTasksSnapshot.data!.where( - (final task) => - sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, - )) ...[ - StreamBuilder( - stream: uploadTask.progress, - builder: (final context, final uploadTaskProgressSnapshot) => - !uploadTaskProgressSnapshot.hasData - ? Container() - : _buildFile( - context: context, - details: FileDetails( - path: uploadTask.path, - isDirectory: false, - size: uploadTask.size, - etag: null, - mimeType: null, - lastModified: uploadTask.lastModified, - hasPreview: null, - isFavorite: null, - ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: null, + child: SortBoxBuilder( + sortBox: filesSortBox, + sortPropertyOption: widget.bloc.options.filesSortPropertyOption, + sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, + input: files.data, + builder: (final context, final sorted) => NeonListView( + scrollKey: 'files-${pathSnapshot.data!.join('/')}', + withFloatingActionButton: true, + items: [ + for (final uploadTask in sorted == null + ? [] + : uploadTasksSnapshot.data!.where( + (final task) => + sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, + )) ...[ + StreamBuilder( + stream: uploadTask.progress, + builder: (final context, final uploadTaskProgressSnapshot) => + !uploadTaskProgressSnapshot.hasData + ? Container() + : _buildFile( + context: context, + details: FileDetails( + path: uploadTask.path, + isDirectory: false, + size: uploadTask.size, + etag: null, + mimeType: null, + lastModified: uploadTask.lastModified, + hasPreview: null, + isFavorite: null, ), - ), - ], - if (sorted != null) ...[ - for (final file in sorted) ...[ - if (!widget.onlyShowDirectories || file.isDirectory) ...[ - Builder( - builder: (final context) { - final matchingUploadTasks = uploadTasksSnapshot.data! - .where((final task) => _pathMatchesFile(task.path, file.name)); - final matchingDownloadTasks = downloadTasksSnapshot.data! - .where((final task) => _pathMatchesFile(task.path, file.name)); + uploadProgress: uploadTaskProgressSnapshot.data, + downloadProgress: null, + ), + ), + ], + if (sorted != null) ...[ + for (final file in sorted) ...[ + if (!widget.onlyShowDirectories || file.isDirectory) ...[ + Builder( + builder: (final context) { + final matchingUploadTasks = uploadTasksSnapshot.data! + .where((final task) => _pathMatchesFile(task.path, file.name)); + final matchingDownloadTasks = downloadTasksSnapshot.data! + .where((final task) => _pathMatchesFile(task.path, file.name)); - return StreamBuilder( - stream: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.progress + return StreamBuilder( + stream: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.progress + : Stream.value(null), + builder: (final context, final uploadTaskProgressSnapshot) => + StreamBuilder( + stream: matchingDownloadTasks.isNotEmpty + ? matchingDownloadTasks.first.progress : Stream.value(null), - builder: (final context, final uploadTaskProgressSnapshot) => - StreamBuilder( - stream: matchingDownloadTasks.isNotEmpty - ? matchingDownloadTasks.first.progress - : Stream.value(null), - builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( - context: context, - details: FileDetails( - path: [...widget.bloc.path.value, file.name], - isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, - size: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.size - : file.size!, - etag: matchingUploadTasks.isNotEmpty ? null : file.etag, - mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, - lastModified: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.lastModified - : file.lastModified!, - hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, - isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, - ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: downloadTaskProgressSnapshot.data, + builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( + context: context, + details: FileDetails( + path: [...widget.bloc.path.value, file.name], + isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, + size: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.size + : file.size!, + etag: matchingUploadTasks.isNotEmpty ? null : file.etag, + mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, + lastModified: matchingUploadTasks.isNotEmpty + ? matchingUploadTasks.first.lastModified + : file.lastModified!, + hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, + isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, ), + uploadProgress: uploadTaskProgressSnapshot.data, + downloadProgress: downloadTaskProgressSnapshot.data, ), - ); - }, - ), - ], + ), + ); + }, + ), ], ], ], - isLoading: files.loading, - error: files.error, - onRefresh: widget.bloc.refresh, - builder: (final context, final widget) => widget, - topScrollingChildren: [ - Align( - alignment: Alignment.topLeft, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, - ), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - SizedBox( - height: 40, - child: InkWell( - onTap: () { - widget.bloc.setPath([]); - }, - child: const Icon(Icons.house), - ), + ], + isLoading: files.loading, + error: files.error, + onRefresh: widget.bloc.refresh, + builder: (final context, final widget) => widget, + topScrollingChildren: [ + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox( + height: 40, + child: InkWell( + onTap: () { + widget.bloc.setPath([]); + }, + child: const Icon(Icons.house), + ), + ), + for (var i = 0; i < pathSnapshot.data!.length; i++) ...[ + InkWell( + onTap: () { + widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1)); + }, + child: Text(pathSnapshot.data![i]), ), - for (var i = 0; i < pathSnapshot.data!.length; i++) ...[ - InkWell( - onTap: () { - widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1)); - }, - child: Text(pathSnapshot.data![i]), + ], + ] + .intersperse( + const Icon( + Icons.keyboard_arrow_right, + size: 40, ), - ], - ] - .intersperse( - const Icon( - Icons.keyboard_arrow_right, - size: 40, - ), - ) - .toList(), - ), + ) + .toList(), ), ), - ], - ), + ), + ], ), ), ), diff --git a/packages/neon/neon_news/lib/neon_news.dart b/packages/neon/neon_news/lib/neon_news.dart index 23696bfa..51dcd88f 100644 --- a/packages/neon/neon_news/lib/neon_news.dart +++ b/packages/neon/neon_news/lib/neon_news.dart @@ -36,8 +36,10 @@ part 'sort/articles.dart'; part 'sort/feeds.dart'; part 'sort/folders.dart'; part 'widgets/articles_view.dart'; +part 'widgets/feed_floating_action_button.dart'; part 'widgets/feed_icon.dart'; part 'widgets/feeds_view.dart'; +part 'widgets/folder_floating_action_button.dart'; part 'widgets/folder_select.dart'; part 'widgets/folder_view.dart'; part 'widgets/folders_view.dart'; diff --git a/packages/neon/neon_news/lib/pages/folder.dart b/packages/neon/neon_news/lib/pages/folder.dart index cb339845..0aa42ffc 100644 --- a/packages/neon/neon_news/lib/pages/folder.dart +++ b/packages/neon/neon_news/lib/pages/folder.dart @@ -20,5 +20,9 @@ class NewsFolderPage extends StatelessWidget { bloc: bloc, folder: folder, ), + floatingActionButton: NewsFeedFloatingActionButton( + bloc: bloc, + folderID: folder.id, + ), ); } diff --git a/packages/neon/neon_news/lib/pages/main.dart b/packages/neon/neon_news/lib/pages/main.dart index 71b77801..12f5d537 100644 --- a/packages/neon/neon_news/lib/pages/main.dart +++ b/packages/neon/neon_news/lib/pages/main.dart @@ -25,41 +25,52 @@ class _NewsMainPageState extends State { } @override - Widget build(final BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - bottomNavigationBar: BottomNavigationBar( - currentIndex: _index, - onTap: (final index) { - setState(() { - _index = index; - }); - }, - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.newspaper), - label: AppLocalizations.of(context).newsArticles, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.folder), - label: AppLocalizations.of(context).newsFolders, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.rss_feed), - label: AppLocalizations.of(context).newsFeeds, - ), - ], - ), - body: _index == 0 - ? NewsArticlesView( - bloc: widget.bloc.mainArticlesBloc, - newsBloc: widget.bloc, - ) - : _index == 1 - ? NewsFoldersView( - bloc: widget.bloc, - ) - : NewsFeedsView( - bloc: widget.bloc, - ), - ); + Widget build(final BuildContext context) { + final views = [ + NewsArticlesView( + bloc: widget.bloc.mainArticlesBloc, + newsBloc: widget.bloc, + ), + NewsFoldersView( + bloc: widget.bloc, + ), + NewsFeedsView( + bloc: widget.bloc, + ), + ]; + + final floatingActionButtons = [ + null, + NewsFolderFloatingActionButton(bloc: widget.bloc), + NewsFeedFloatingActionButton(bloc: widget.bloc), + ]; + + return Scaffold( + resizeToAvoidBottomInset: false, + bottomNavigationBar: BottomNavigationBar( + currentIndex: _index, + onTap: (final index) { + setState(() { + _index = index; + }); + }, + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.newspaper), + label: AppLocalizations.of(context).newsArticles, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.folder), + label: AppLocalizations.of(context).newsFolders, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.rss_feed), + label: AppLocalizations.of(context).newsFeeds, + ), + ], + ), + body: views[_index], + floatingActionButton: floatingActionButtons[_index], + ); + } } diff --git a/packages/neon/neon_news/lib/widgets/articles_view.dart b/packages/neon/neon_news/lib/widgets/articles_view.dart index 30dc112f..d6cb291f 100644 --- a/packages/neon/neon_news/lib/widgets/articles_view.dart +++ b/packages/neon/neon_news/lib/widgets/articles_view.dart @@ -29,73 +29,70 @@ class _NewsArticlesViewState extends State { stream: widget.newsBloc.feeds, builder: (final context, final feeds) => ResultBuilder>( stream: widget.bloc.articles, - builder: (final context, final articles) => Scaffold( - resizeToAvoidBottomInset: false, - body: SortBoxBuilder( - sortBox: articlesSortBox, - sortPropertyOption: widget.newsBloc.options.articlesSortPropertyOption, - sortBoxOrderOption: widget.newsBloc.options.articlesSortBoxOrderOption, - input: articles.data, - builder: (final context, final sorted) => NeonListView( - scrollKey: 'news-articles', - items: feeds.data == null ? null : sorted, - isLoading: articles.loading || feeds.loading, - error: articles.error ?? feeds.error, - onRefresh: () async { - await Future.wait([ - widget.bloc.refresh(), - widget.newsBloc.refresh(), - ]); - }, - builder: (final context, final article) => _buildArticle( - context, - article, - feeds.data!.singleWhere((final feed) => feed.id == article.feedId), - ), - topFixedChildren: [ - StreamBuilder( - stream: widget.bloc.filterType, - builder: (final context, final selectedFilterTypeSnapshot) => Container( - margin: const EdgeInsets.symmetric(horizontal: 15), - child: DropdownButton( - isExpanded: true, - value: selectedFilterTypeSnapshot.data, - items: [ - FilterType.all, - FilterType.unread, - if (widget.bloc.listType == null) ...[ - FilterType.starred, - ], - ].map>( - (final a) { - late final String label; - switch (a) { - case FilterType.all: - label = AppLocalizations.of(context).newsFilterAll; - break; - case FilterType.unread: - label = AppLocalizations.of(context).newsFilterUnread; - break; - case FilterType.starred: - label = AppLocalizations.of(context).newsFilterStarred; - break; - default: - throw Exception('FilterType $a should not be shown'); - } - return DropdownMenuItem( - value: a, - child: Text(label), - ); - }, - ).toList(), - onChanged: (final value) { - widget.bloc.setFilterType(value!); + builder: (final context, final articles) => SortBoxBuilder( + sortBox: articlesSortBox, + sortPropertyOption: widget.newsBloc.options.articlesSortPropertyOption, + sortBoxOrderOption: widget.newsBloc.options.articlesSortBoxOrderOption, + input: articles.data, + builder: (final context, final sorted) => NeonListView( + scrollKey: 'news-articles', + items: feeds.data == null ? null : sorted, + isLoading: articles.loading || feeds.loading, + error: articles.error ?? feeds.error, + onRefresh: () async { + await Future.wait([ + widget.bloc.refresh(), + widget.newsBloc.refresh(), + ]); + }, + builder: (final context, final article) => _buildArticle( + context, + article, + feeds.data!.singleWhere((final feed) => feed.id == article.feedId), + ), + topFixedChildren: [ + StreamBuilder( + stream: widget.bloc.filterType, + builder: (final context, final selectedFilterTypeSnapshot) => Container( + margin: const EdgeInsets.symmetric(horizontal: 15), + child: DropdownButton( + isExpanded: true, + value: selectedFilterTypeSnapshot.data, + items: [ + FilterType.all, + FilterType.unread, + if (widget.bloc.listType == null) ...[ + FilterType.starred, + ], + ].map>( + (final a) { + late final String label; + switch (a) { + case FilterType.all: + label = AppLocalizations.of(context).newsFilterAll; + break; + case FilterType.unread: + label = AppLocalizations.of(context).newsFilterUnread; + break; + case FilterType.starred: + label = AppLocalizations.of(context).newsFilterStarred; + break; + default: + throw Exception('FilterType $a should not be shown'); + } + return DropdownMenuItem( + value: a, + child: Text(label), + ); }, - ), + ).toList(), + onChanged: (final value) { + widget.bloc.setFilterType(value!); + }, ), ), - ], - ), + ), + ], ), ), ), diff --git a/packages/neon/neon_news/lib/widgets/feed_floating_action_button.dart b/packages/neon/neon_news/lib/widgets/feed_floating_action_button.dart new file mode 100644 index 00000000..d48a30a7 --- /dev/null +++ b/packages/neon/neon_news/lib/widgets/feed_floating_action_button.dart @@ -0,0 +1,29 @@ +part of '../neon_news.dart'; + +class NewsFeedFloatingActionButton extends StatelessWidget { + const NewsFeedFloatingActionButton({ + required this.bloc, + this.folderID, + super.key, + }); + + final NewsBloc bloc; + final int? folderID; + + @override + Widget build(final BuildContext context) => FloatingActionButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (final context) => NewsAddFeedDialog( + bloc: bloc, + folderID: folderID, + ), + ); + if (result != null) { + bloc.addFeed(result[0] as String, result[1] as int?); + } + }, + child: const Icon(Icons.add), + ); +} diff --git a/packages/neon/neon_news/lib/widgets/feeds_view.dart b/packages/neon/neon_news/lib/widgets/feeds_view.dart index be769ca2..bfbee64f 100644 --- a/packages/neon/neon_news/lib/widgets/feeds_view.dart +++ b/packages/neon/neon_news/lib/widgets/feeds_view.dart @@ -15,42 +15,24 @@ class NewsFeedsView extends StatelessWidget { stream: bloc.folders, builder: (final context, final folders) => ResultBuilder>( stream: bloc.feeds, - builder: (final context, final feeds) => Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (final context) => NewsAddFeedDialog( - bloc: bloc, - folderID: folderID, - ), - ); - if (result != null) { - bloc.addFeed(result[0] as String, result[1] as int?); - } - }, - child: const Icon(Icons.add), - ), - body: SortBoxBuilder( - sortBox: feedsSortBox, - sortPropertyOption: bloc.options.feedsSortPropertyOption, - sortBoxOrderOption: bloc.options.feedsSortBoxOrderOption, - input: folders.data == null - ? null - : feeds.data?.where((final f) => folderID == null || f.folderId == folderID).toList(), - builder: (final context, final sorted) => NeonListView( - scrollKey: 'news-feeds', - withFloatingActionButton: true, - items: sorted, - isLoading: feeds.loading || folders.loading, - error: feeds.error ?? folders.error, - onRefresh: bloc.refresh, - builder: (final context, final feed) => _buildFeed( - context, - feed, - folders.data!, - ), + builder: (final context, final feeds) => SortBoxBuilder( + sortBox: feedsSortBox, + sortPropertyOption: bloc.options.feedsSortPropertyOption, + sortBoxOrderOption: bloc.options.feedsSortBoxOrderOption, + input: folders.data == null + ? null + : feeds.data?.where((final f) => folderID == null || f.folderId == folderID).toList(), + builder: (final context, final sorted) => NeonListView( + scrollKey: 'news-feeds', + withFloatingActionButton: true, + items: sorted, + isLoading: feeds.loading || folders.loading, + error: feeds.error ?? folders.error, + onRefresh: bloc.refresh, + builder: (final context, final feed) => _buildFeed( + context, + feed, + folders.data!, ), ), ), diff --git a/packages/neon/neon_news/lib/widgets/folder_floating_action_button.dart b/packages/neon/neon_news/lib/widgets/folder_floating_action_button.dart new file mode 100644 index 00000000..147f45bc --- /dev/null +++ b/packages/neon/neon_news/lib/widgets/folder_floating_action_button.dart @@ -0,0 +1,24 @@ +part of '../neon_news.dart'; + +class NewsFolderFloatingActionButton extends StatelessWidget { + const NewsFolderFloatingActionButton({ + required this.bloc, + super.key, + }); + + final NewsBloc bloc; + + @override + Widget build(final BuildContext context) => FloatingActionButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (final context) => const NewsCreateFolderDialog(), + ); + if (result != null) { + bloc.createFolder(result); + } + }, + child: const Icon(Icons.add), + ); +} diff --git a/packages/neon/neon_news/lib/widgets/folders_view.dart b/packages/neon/neon_news/lib/widgets/folders_view.dart index 82f110ca..edf2df99 100644 --- a/packages/neon/neon_news/lib/widgets/folders_view.dart +++ b/packages/neon/neon_news/lib/widgets/folders_view.dart @@ -9,47 +9,32 @@ class NewsFoldersView extends StatelessWidget { final NewsBloc bloc; @override - Widget build(final BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (final context) => const NewsCreateFolderDialog(), - ); - if (result != null) { - bloc.createFolder(result); - } - }, - child: const Icon(Icons.add), - ), - body: ResultBuilder>( - stream: bloc.folders, - builder: (final context, final folders) => ResultBuilder>( - stream: bloc.feeds, - builder: (final context, final feeds) => SortBoxBuilder( - sortBox: foldersSortBox, - sortPropertyOption: bloc.options.foldersSortPropertyOption, - sortBoxOrderOption: bloc.options.foldersSortBoxOrderOption, - input: feeds.data == null - ? null - : folders.data - ?.map( - (final folder) => FolderFeedsWrapper( - folder, - feeds.data!.where((final feed) => feed.folderId == folder.id).toList(), - ), - ) - .toList(), - builder: (final context, final sorted) => NeonListView( - scrollKey: 'news-folders', - withFloatingActionButton: true, - items: sorted, - isLoading: feeds.loading || folders.loading, - error: feeds.error ?? folders.error, - onRefresh: bloc.refresh, - builder: _buildFolder, - ), + Widget build(final BuildContext context) => ResultBuilder>( + stream: bloc.folders, + builder: (final context, final folders) => ResultBuilder>( + stream: bloc.feeds, + builder: (final context, final feeds) => SortBoxBuilder( + sortBox: foldersSortBox, + sortPropertyOption: bloc.options.foldersSortPropertyOption, + sortBoxOrderOption: bloc.options.foldersSortBoxOrderOption, + input: feeds.data == null + ? null + : folders.data + ?.map( + (final folder) => FolderFeedsWrapper( + folder, + feeds.data!.where((final feed) => feed.folderId == folder.id).toList(), + ), + ) + .toList(), + builder: (final context, final sorted) => NeonListView( + scrollKey: 'news-folders', + withFloatingActionButton: true, + items: sorted, + isLoading: feeds.loading || folders.loading, + error: feeds.error ?? folders.error, + onRefresh: bloc.refresh, + builder: _buildFolder, ), ), ), diff --git a/packages/neon/neon_notes/lib/neon_notes.dart b/packages/neon/neon_notes/lib/neon_notes.dart index 032356d2..c3fc8818 100644 --- a/packages/neon/neon_notes/lib/neon_notes.dart +++ b/packages/neon/neon_notes/lib/neon_notes.dart @@ -31,6 +31,7 @@ part 'utils/category_color.dart'; part 'utils/exception_handler.dart'; part 'widgets/categories_view.dart'; part 'widgets/category_select.dart'; +part 'widgets/notes_floating_action_button.dart'; part 'widgets/notes_view.dart'; class NotesApp extends AppImplementation { diff --git a/packages/neon/neon_notes/lib/pages/category.dart b/packages/neon/neon_notes/lib/pages/category.dart index 5fa059c2..ec013547 100644 --- a/packages/neon/neon_notes/lib/pages/category.dart +++ b/packages/neon/neon_notes/lib/pages/category.dart @@ -20,5 +20,9 @@ class NotesCategoryPage extends StatelessWidget { bloc: bloc, category: category.name, ), + floatingActionButton: NotesFloatingActionButton( + bloc: bloc, + category: category.name, + ), ); } diff --git a/packages/neon/neon_notes/lib/pages/main.dart b/packages/neon/neon_notes/lib/pages/main.dart index 43c45bc4..20ab3589 100644 --- a/packages/neon/neon_notes/lib/pages/main.dart +++ b/packages/neon/neon_notes/lib/pages/main.dart @@ -25,32 +25,43 @@ class _NotesMainPageState extends State { } @override - Widget build(final BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - bottomNavigationBar: BottomNavigationBar( - currentIndex: _index, - onTap: (final index) { - setState(() { - _index = index; - }); - }, - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.note), - label: AppLocalizations.of(context).notesNotes, - ), - BottomNavigationBarItem( - icon: const Icon(MdiIcons.tag), - label: AppLocalizations.of(context).notesCategories, - ), - ], - ), - body: _index == 0 - ? NotesView( - bloc: widget.bloc, - ) - : NotesCategoriesView( - bloc: widget.bloc, - ), - ); + Widget build(final BuildContext context) { + final views = [ + NotesView( + bloc: widget.bloc, + ), + NotesCategoriesView( + bloc: widget.bloc, + ), + ]; + + final floatingActionButtons = [ + NotesFloatingActionButton(bloc: widget.bloc), + null, + ]; + + return Scaffold( + resizeToAvoidBottomInset: false, + bottomNavigationBar: BottomNavigationBar( + currentIndex: _index, + onTap: (final index) { + setState(() { + _index = index; + }); + }, + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.note), + label: AppLocalizations.of(context).notesNotes, + ), + BottomNavigationBarItem( + icon: const Icon(MdiIcons.tag), + label: AppLocalizations.of(context).notesCategories, + ), + ], + ), + body: views[_index], + floatingActionButton: floatingActionButtons[_index], + ); + } } diff --git a/packages/neon/neon_notes/lib/widgets/notes_floating_action_button.dart b/packages/neon/neon_notes/lib/widgets/notes_floating_action_button.dart new file mode 100644 index 00000000..141e6b26 --- /dev/null +++ b/packages/neon/neon_notes/lib/widgets/notes_floating_action_button.dart @@ -0,0 +1,32 @@ +part of '../neon_notes.dart'; + +class NotesFloatingActionButton extends StatelessWidget { + const NotesFloatingActionButton({ + required this.bloc, + this.category, + super.key, + }); + + final NotesBloc bloc; + final String? category; + + @override + Widget build(final BuildContext context) => FloatingActionButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (final context) => NotesCreateNoteDialog( + bloc: bloc, + category: category, + ), + ); + if (result != null) { + bloc.createNote( + title: result[0] as String, + category: result[1] as String? ?? '', + ); + } + }, + child: const Icon(Icons.add), + ); +} diff --git a/packages/neon/neon_notes/lib/widgets/notes_view.dart b/packages/neon/neon_notes/lib/widgets/notes_view.dart index f3e204c2..bf138478 100644 --- a/packages/neon/neon_notes/lib/widgets/notes_view.dart +++ b/packages/neon/neon_notes/lib/widgets/notes_view.dart @@ -13,52 +13,31 @@ class NotesView extends StatelessWidget { @override Widget build(final BuildContext context) => ResultBuilder>( stream: bloc.notes, - builder: (final context, final notes) => Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (final context) => NotesCreateNoteDialog( - bloc: bloc, - category: category, - ), - ); - if (result != null) { - bloc.createNote( - title: result[0] as String, - category: result[1] as String? ?? '', - ); - } - }, - child: const Icon(Icons.add), - ), - body: SortBoxBuilder( + builder: (final context, final notes) => SortBoxBuilder( + sortBox: notesSortBox, + sortPropertyOption: bloc.options.notesSortPropertyOption, + sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, + input: category != null + ? notes.data?.where((final note) => note.favorite && note.category == category).toList() + : notes.data?.where((final note) => note.favorite).toList(), + builder: (final context, final sortedFavorites) => SortBoxBuilder( sortBox: notesSortBox, sortPropertyOption: bloc.options.notesSortPropertyOption, sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, input: category != null - ? notes.data?.where((final note) => note.favorite && note.category == category).toList() - : notes.data?.where((final note) => note.favorite).toList(), - builder: (final context, final sortedFavorites) => SortBoxBuilder( - sortBox: notesSortBox, - sortPropertyOption: bloc.options.notesSortPropertyOption, - sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, - input: category != null - ? notes.data?.where((final note) => !note.favorite && note.category == category).toList() - : notes.data?.where((final note) => !note.favorite).toList(), - builder: (final context, final sortedNonFavorites) => NeonListView( - scrollKey: 'notes-notes', - withFloatingActionButton: true, - items: [ - ...?sortedFavorites, - ...?sortedNonFavorites, - ], - isLoading: notes.loading, - error: notes.error, - onRefresh: bloc.refresh, - builder: _buildNote, - ), + ? notes.data?.where((final note) => !note.favorite && note.category == category).toList() + : notes.data?.where((final note) => !note.favorite).toList(), + builder: (final context, final sortedNonFavorites) => NeonListView( + scrollKey: 'notes-notes', + withFloatingActionButton: true, + items: [ + ...?sortedFavorites, + ...?sortedNonFavorites, + ], + isLoading: notes.loading, + error: notes.error, + onRefresh: bloc.refresh, + builder: _buildNote, ), ), ),