diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index 64f59c0..b171c7a 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -90,6 +90,7 @@ class _SCNAppBarState extends State { )); } else { actions.add(_buildSpacer()); + actions.add(_buildSpacer()); } return Consumer(builder: (context, value, child) { diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart index 9588fa4..a2d1c6f 100644 --- a/flutter/lib/models/channel.dart +++ b/flutter/lib/models/channel.dart @@ -74,7 +74,7 @@ class Channel extends HiveObject implements FieldDebuggable { class ChannelWithSubscription { final Channel channel; - final Subscription subscription; + final Subscription? subscription; ChannelWithSubscription({ required this.channel, @@ -84,7 +84,7 @@ class ChannelWithSubscription { factory ChannelWithSubscription.fromJson(Map json) { return ChannelWithSubscription( channel: Channel.fromJson(json), - subscription: Subscription.fromJson(json['subscription'] as Map), + subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map), ); } diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index b08b90f..cd9dafd 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -59,7 +59,7 @@ class _SCNNavLayoutState extends State { return Scaffold( appBar: SCNAppBar( title: null, - showSearch: _selectedIndex == 0 || _selectedIndex == 1, + showSearch: _selectedIndex == 0, showShare: false, showThemeSwitch: true, ), diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index fa0455f..f118a90 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -3,10 +3,12 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; class ChannelRootPage extends StatefulWidget { const ChannelRootPage({super.key, required this.isVisiblePage}); @@ -18,7 +20,7 @@ class ChannelRootPage extends StatefulWidget { } class _ChannelRootPageState extends State { - final PagingController _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0); + final PagingController _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0); bool _isInitialized = false; @@ -68,9 +70,9 @@ class _ChannelRootPageState extends State { } try { - final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).map((p) => p.channel).toList(); + final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList(); - items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? '')); + items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? '')); _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null); } catch (exc, trace) { @@ -94,9 +96,9 @@ class _ChannelRootPageState extends State { AppBarState().setLoadingIndeterminate(true); - final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).map((p) => p.channel).toList(); + final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList(); - items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? '')); + items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? '')); _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null); } catch (exc, trace) { @@ -113,12 +115,15 @@ class _ChannelRootPageState extends State { onRefresh: () => Future.sync( () => _pagingController.refresh(), ), - child: PagedListView( + child: PagedListView( pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( + builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) => ChannelListItem( - channel: item, - onPressed: () {/*TODO*/}, + channel: item.channel, + subscription: item.subscription, + onPressed: () { + Navi.push(context, () => ChannelViewPage(channel: item.channel, subscription: item.subscription)); + }, ), ), ), diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index 11e715c..5aeb4fc 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; class ChannelListItem extends StatefulWidget { @@ -12,10 +14,12 @@ class ChannelListItem extends StatefulWidget { const ChannelListItem({ required this.channel, required this.onPressed, + required this.subscription, super.key, }); final Channel channel; + final Subscription? subscription; final Null Function() onPressed; @override @@ -53,35 +57,43 @@ class _ChannelListItemState extends State { onTap: widget.onPressed, child: Padding( padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Row( children: [ - Row( - children: [ - Expanded( - child: Text( - widget.channel.displayName, - style: const TextStyle(fontWeight: FontWeight.bold), + _buildIcon(context), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.channel.displayName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Text( + (widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()), + style: const TextStyle(fontSize: 14), + ), + ], ), - ), - Text( - (widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()), - style: const TextStyle(fontSize: 14), - ), - ], - ), - SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Text( - _preformatTitle(lastMessage), - style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)), + SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + _preformatTitle(lastMessage), + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)), + ), + ), + Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + ], ), - ), - Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - ], + ], + ), ), ], ), @@ -94,4 +106,14 @@ class _ChannelListItemState extends State { if (message == null) return '...'; return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); } + + Widget _buildIcon(BuildContext context) { + if (widget.subscription == null) { + return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed + } else if (widget.subscription!.confirmed) { + return Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed + } else { + return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested + } + } } diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart new file mode 100644 index 0000000..5a76d92 --- /dev/null +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.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/keytoken.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/models/subscription.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/app_bar_state.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; + +class ChannelViewPage extends StatefulWidget { + const ChannelViewPage({ + required this.channel, + required this.subscription, + super.key, + }); + + final Channel channel; + final Subscription? subscription; + + @override + State createState() => _ChannelViewPageState(); +} + +class _ChannelViewPageState extends State { + static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'Channel', + showSearch: false, + showShare: false, + child: _buildChannelView(context), + ); + } + + Widget _buildChannelView(BuildContext context) { + final userAccUserID = context.select((v) => v.userID); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ..._buildChannelHeader(context), + SizedBox(height: 8), + _buildQRCode(context), + SizedBox(height: 8), + //TODO icons + _buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'ChannelID', ['...'], null), + _buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'InternalName', ['...'], null), + _buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'DisplayName', ['...'], null), //TODO edit icon on right to edit name + _buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'Subscription (own)', ['...'], null), //TODO sub/unsub icon on right + //TODO list foreign subscriptions (with accept/decline/delete button on right) + _buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'Messages', ['...'], () {/*TODO*/}), + ], + ), + ), + ); + } + + List _buildChannelHeader(BuildContext context) { + return [ + Text(widget.channel.displayName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ]; + } + + Widget _buildMetaCard(BuildContext context, IconData icn, String title, List values, void Function()? action) { + final container = UI.box( + context: context, + padding: EdgeInsets.fromLTRB(16, 2, 4, 2), + child: Row( + children: [ + FaIcon(icn, size: 18), + SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + for (final val in values) Text(val, style: const TextStyle(fontSize: 14)), + ], + ), + ], + ), + ); + + if (action == null) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: container, + ); + } else { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: InkWell( + splashColor: Theme.of(context).splashColor, + onTap: action, + child: container, + ), + ); + } + } + + String _preformatTitle(SCNMessage message) { + return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); + } + + String _prettyPrintPriority(int priority) { + switch (priority) { + case 0: + return 'Low (0)'; + case 1: + return 'Normal (1)'; + case 2: + return 'High (2)'; + default: + return 'Unknown ($priority)'; + } + } + + Widget _buildQRCode(BuildContext context) { + var text = 'TODO' + widget.channel.channelID; //TODO subkey+channelid with deeplink-y + return GestureDetector( + onTap: () { + //TODO share + }, + child: Center( + child: QrImageView( + data: text, + version: QrVersions.auto, + size: 300.0, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 9dd5ea2..bbe4cb0 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -114,7 +114,7 @@ class _MessageListPageState extends State with RouteAware { } void _onLifecycleResume() { - if (AppSettings().alwaysBackgroundRefreshMessageListOnLifecycleResume) { + if (AppSettings().alwaysBackgroundRefreshMessageListOnLifecycleResume && widget.isVisiblePage) { ApplicationLog.debug('[MessageList::_onLifecycleResume] --> (will background-refresh)'); _backgroundRefresh(false); }