From 6ec1d80f49621a8d78715496842f7ee0396ea3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= <pubgit@mikescher.com> Date: Sun, 13 Apr 2025 17:43:18 +0200 Subject: [PATCH] finish sender_list && plain-text-search --- flutter/lib/api/api_client.dart | 3 + .../layout/app_bar_filter_dialog.dart | 7 +- .../modals/filter_modal_searchplain.dart | 60 +++++++++ .../modals/filter_modal_sendername.dart | 6 +- .../filtered_message_view.dart | 120 ++++++++++++++++++ .../message_list/message_filter_chiplet.dart | 3 + .../lib/pages/message_list/message_list.dart | 20 ++- .../lib/pages/message_view/message_view.dart | 5 +- .../pages/sender_list/sender_list_item.dart | 7 +- 9 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 flutter/lib/components/modals/filter_modal_searchplain.dart create mode 100644 flutter/lib/pages/filtered_message_view/filtered_message_view.dart diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index cc474a9..ac57d21 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -32,6 +32,7 @@ enum ChannelSelector { class MessageFilter { List<String>? channelIDs; List<String>? searchFilter; + List<String>? plainSearchFilter; List<String>? senderNames; List<String>? usedKeys; List<int>? priority; @@ -42,6 +43,7 @@ class MessageFilter { MessageFilter({ this.channelIDs, this.searchFilter, + this.plainSearchFilter, this.senderNames, this.usedKeys, this.priority, @@ -288,6 +290,7 @@ class APIClient { 'next_page_token': [pageToken], if (pageSize != null) 'page_size': [pageSize.toString()], if (filter?.searchFilter != null) 'search': filter!.searchFilter!, + if (filter?.plainSearchFilter != null) 'string_search': filter!.plainSearchFilter!, if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!, if (filter?.senderNames != null) 'sender': filter!.senderNames!, if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()], diff --git a/flutter/lib/components/layout/app_bar_filter_dialog.dart b/flutter/lib/components/layout/app_bar_filter_dialog.dart index 2b7489e..547825f 100644 --- a/flutter/lib/components/layout/app_bar_filter_dialog.dart +++ b/flutter/lib/components/layout/app_bar_filter_dialog.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simplecloudnotifier/components/modals/filter_modal_channel.dart'; import 'package:simplecloudnotifier/components/modals/filter_modal_keytoken.dart'; import 'package:simplecloudnotifier/components/modals/filter_modal_priority.dart'; +import 'package:simplecloudnotifier/components/modals/filter_modal_searchplain.dart'; import 'package:simplecloudnotifier/components/modals/filter_modal_sendername.dart'; import 'package:simplecloudnotifier/components/modals/filter_modal_time.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; @@ -16,7 +17,9 @@ class AppBarFilterDialog extends StatefulWidget { class _AppBarFilterDialogState extends State<AppBarFilterDialog> { double _height = 0; - double _targetHeight = 4 + (48 * 6) + (16 * 5) + 4; + static const int _itemCount = 7; + + static const double _targetHeight = 4 + (48 * _itemCount) + (16 * (_itemCount - 1)) + 4; @override void initState() { @@ -117,6 +120,6 @@ class _AppBarFilterDialogState extends State<AppBarFilterDialog> { } void _showPlainSearchModal(BuildContext context) { - //TODO showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain()); + showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain()); } } diff --git a/flutter/lib/components/modals/filter_modal_searchplain.dart b/flutter/lib/components/modals/filter_modal_searchplain.dart new file mode 100644 index 0000000..28f60c6 --- /dev/null +++ b/flutter/lib/components/modals/filter_modal_searchplain.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart'; +import 'package:simplecloudnotifier/state/app_events.dart'; + +class FilterModalSearchPlain extends StatefulWidget { + @override + _FilterModalSearchPlainState createState() => _FilterModalSearchPlainState(); +} + +class _FilterModalSearchPlainState extends State<FilterModalSearchPlain> { + final _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Search'), + content: Container( + child: TextField( + autofocus: true, + controller: _controller, + decoration: InputDecoration(hintText: "Search..."), + ), + ), + actions: <Widget>[ + TextButton( + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), + child: const Text('Apply'), + onPressed: _onOkay, + ), + ], + ); + } + + void _onOkay() { + Navigator.of(context).pop(); + + List<MessageFilterChiplet> chiplets = []; + if (_controller.text.isNotEmpty) { + chiplets.add(MessageFilterChiplet( + label: _controller.text, + value: _controller.text, + type: MessageFilterChipletType.plainSearch, + )); + } + + AppEvents().notifyFilterListeners([MessageFilterChipletType.plainSearch], chiplets); + } +} diff --git a/flutter/lib/components/modals/filter_modal_sendername.dart b/flutter/lib/components/modals/filter_modal_sendername.dart index 3cdfd7a..7a1c8c7 100644 --- a/flutter/lib/components/modals/filter_modal_sendername.dart +++ b/flutter/lib/components/modals/filter_modal_sendername.dart @@ -73,15 +73,13 @@ class _FilterModalSendernameState extends State<FilterModalSendername> { TextButton( style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: const Text('Apply'), - onPressed: () { - onOkay(); - }, + onPressed: _onOkay, ), ], ); } - void onOkay() { + void _onOkay() { Navigator.of(context).pop(); final chiplets = _selectedEntries diff --git a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart new file mode 100644 index 0000000..5efeda4 --- /dev/null +++ b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; +import 'package:simplecloudnotifier/pages/message_view/message_view.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/scn_data_cache.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:provider/provider.dart'; + +class FilteredMessageViewPage extends StatefulWidget { + const FilteredMessageViewPage({ + required this.title, + required this.filter, + super.key, + }); + + final String title; + final MessageFilter filter; + + @override + State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState(); +} + +class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> { + PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start'); + + Map<String, Channel>? _channels = null; + bool _channelsFetched = false; + + @override + void initState() { + super.initState(); + + _channels = SCNDataCache().getChannelMap(); + + _pagingController.addPageRequestListener(_fetchPage); + + _pagingController.refresh(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + Future<void> _fetchPage(String thisPageToken) async { + final acc = Provider.of<AppAuth>(context, listen: false); + final cfg = Provider.of<AppSettings>(context, listen: false); + + ApplicationLog.debug('Start FilteredMessageViewPage::_pagingController::_fetchPage [ ${thisPageToken} ]'); + + if (!acc.isAuth()) { + _pagingController.error = 'Not logged in'; + return; + } + + try { + if (_channels == null || !_channelsFetched) { + final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); + setState(() { + _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; + _channelsFetched = true; + }); + } + + final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter); + + SCNDataCache().addToMessageCache(newItems); // no await + + if (npt == '@end') { + _pagingController.appendLastPage(newItems); + } else { + _pagingController.appendPage(newItems, npt); + } + } catch (exc, trace) { + _pagingController.error = exc.toString(); + ApplicationLog.error('Failed to list channel-messages: ' + exc.toString(), trace: trace); + } + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: this.widget.title, + showSearch: false, + showShare: false, + child: _buildMessageList(context), + ); + } + + Widget _buildMessageList(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(8, 4, 8, 4), + child: RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView<String, SCNMessage>( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate<SCNMessage>( + itemBuilder: (context, item, index) => MessageListItem( + message: item, + allChannels: _channels ?? {}, + onPressed: () { + Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,))); + }, + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/pages/message_list/message_filter_chiplet.dart b/flutter/lib/pages/message_list/message_filter_chiplet.dart index cfc8220..0d07501 100644 --- a/flutter/lib/pages/message_list/message_filter_chiplet.dart +++ b/flutter/lib/pages/message_list/message_filter_chiplet.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; enum MessageFilterChipletType { search, + plainSearch, channel, sender, timeRange, @@ -21,6 +22,8 @@ class MessageFilterChiplet { switch (type) { case MessageFilterChipletType.search: return FontAwesomeIcons.magnifyingGlass; + case MessageFilterChipletType.plainSearch: + return FontAwesomeIcons.magnifyingGlassPlus; case MessageFilterChipletType.channel: return FontAwesomeIcons.snake; case MessageFilterChipletType.sender: diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 2126611..84293ae 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -30,6 +30,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware { PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start'); Map<String, Channel>? _channels = null; + bool _channelsFetched = false; bool _isInitialized = false; @@ -135,9 +136,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware { } try { - if (_channels == null) { + if (_channels == null || !_channelsFetched) { final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); - _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; + setState(() { + _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; + _channelsFetched = true; + }); SCNDataCache().setChannelCache(channels); // no await } @@ -314,6 +318,11 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware { filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList(); } + var chipletsPlainSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.plainSearch).toList(); + if (chipletsPlainSearch.isNotEmpty) { + filter.plainSearchFilter = chipletsPlainSearch.map((p) => p.value as String).toList(); + } + var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList(); if (chipletsKeyTokens.isNotEmpty) { filter.usedKeys = chipletsKeyTokens.map((p) => p.value as String).toList(); @@ -329,6 +338,13 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware { filter.senderNames = chipletSender.map((p) => p.value as String).toList(); } + var chipletsTimeRange = _filterChiplets.where((p) => p.type == MessageFilterChipletType.timeRange).toList(); + if (chipletsTimeRange.isNotEmpty) { + //TODO + //filter.timeAfter = chipletsTimeRange[0].value1 as DateTime; + //filter.timeBefore = chipletsTimeRange[0].value2 as DateTime; + } + return filter; } } diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index efe840a..f34624e 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -11,6 +11,7 @@ import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; +import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; @@ -152,7 +153,9 @@ class _MessageViewPageState extends State<MessageViewPage> { icon: FontAwesomeIcons.solidSignature, title: 'Sender', values: [message.senderName!], - mainAction: () => {/*TODO*/}, + mainAction: () => { + Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!]))) + }, ), UI.metaCard( context: context, diff --git a/flutter/lib/pages/sender_list/sender_list_item.dart b/flutter/lib/pages/sender_list/sender_list_item.dart index 70b6c14..3e9df98 100644 --- a/flutter/lib/pages/sender_list/sender_list_item.dart +++ b/flutter/lib/pages/sender_list/sender_list_item.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/sender_name_statistics.dart'; +import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; enum SenderListItemMode { @@ -27,8 +29,7 @@ class SenderListItem extends StatelessWidget { color: Theme.of(context).cardTheme.color, child: InkWell( onTap: () { - //TODO - Navi.popToRoot(context); + Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name]))); }, child: Padding( padding: const EdgeInsets.all(8), @@ -69,7 +70,7 @@ class SenderListItem extends StatelessWidget { SizedBox(width: 4), GestureDetector( onTap: () { - //TODO + Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name]))); }, child: Padding( padding: const EdgeInsets.all(8),