Channel List/view WIP

This commit is contained in:
Mike Schwörer 2024-06-25 12:00:34 +02:00
parent 7dad61dbbb
commit e2dbe8866d
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 232 additions and 39 deletions

View File

@ -90,6 +90,7 @@ class _SCNAppBarState extends State<SCNAppBar> {
)); ));
} else { } else {
actions.add(_buildSpacer()); actions.add(_buildSpacer());
actions.add(_buildSpacer());
} }
return Consumer<AppBarState>(builder: (context, value, child) { return Consumer<AppBarState>(builder: (context, value, child) {

View File

@ -74,7 +74,7 @@ class Channel extends HiveObject implements FieldDebuggable {
class ChannelWithSubscription { class ChannelWithSubscription {
final Channel channel; final Channel channel;
final Subscription subscription; final Subscription? subscription;
ChannelWithSubscription({ ChannelWithSubscription({
required this.channel, required this.channel,
@ -84,7 +84,7 @@ class ChannelWithSubscription {
factory ChannelWithSubscription.fromJson(Map<String, dynamic> json) { factory ChannelWithSubscription.fromJson(Map<String, dynamic> json) {
return ChannelWithSubscription( return ChannelWithSubscription(
channel: Channel.fromJson(json), channel: Channel.fromJson(json),
subscription: Subscription.fromJson(json['subscription'] as Map<String, dynamic>), subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
); );
} }

View File

@ -59,7 +59,7 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
return Scaffold( return Scaffold(
appBar: SCNAppBar( appBar: SCNAppBar(
title: null, title: null,
showSearch: _selectedIndex == 0 || _selectedIndex == 1, showSearch: _selectedIndex == 0,
showShare: false, showShare: false,
showThemeSwitch: true, showThemeSwitch: true,
), ),

View File

@ -3,10 +3,12 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.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/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class ChannelRootPage extends StatefulWidget { class ChannelRootPage extends StatefulWidget {
const ChannelRootPage({super.key, required this.isVisiblePage}); const ChannelRootPage({super.key, required this.isVisiblePage});
@ -18,7 +20,7 @@ class ChannelRootPage extends StatefulWidget {
} }
class _ChannelRootPageState extends State<ChannelRootPage> { class _ChannelRootPageState extends State<ChannelRootPage> {
final PagingController<int, Channel> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0); final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
bool _isInitialized = false; bool _isInitialized = false;
@ -68,9 +70,9 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
} }
try { 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); _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
} catch (exc, trace) { } catch (exc, trace) {
@ -94,9 +96,9 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
AppBarState().setLoadingIndeterminate(true); 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); _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
} catch (exc, trace) { } catch (exc, trace) {
@ -113,12 +115,15 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
onRefresh: () => Future.sync( onRefresh: () => Future.sync(
() => _pagingController.refresh(), () => _pagingController.refresh(),
), ),
child: PagedListView<int, Channel>( child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController, pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Channel>( builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem( itemBuilder: (context, item, index) => ChannelListItem(
channel: item, channel: item.channel,
onPressed: () {/*TODO*/}, subscription: item.subscription,
onPressed: () {
Navi.push(context, () => ChannelViewPage(channel: item.channel, subscription: item.subscription));
},
), ),
), ),
), ),

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
class ChannelListItem extends StatefulWidget { class ChannelListItem extends StatefulWidget {
@ -12,10 +14,12 @@ class ChannelListItem extends StatefulWidget {
const ChannelListItem({ const ChannelListItem({
required this.channel, required this.channel,
required this.onPressed, required this.onPressed,
required this.subscription,
super.key, super.key,
}); });
final Channel channel; final Channel channel;
final Subscription? subscription;
final Null Function() onPressed; final Null Function() onPressed;
@override @override
@ -53,35 +57,43 @@ class _ChannelListItemState extends State<ChannelListItem> {
onTap: widget.onPressed, onTap: widget.onPressed,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( _buildIcon(context),
children: [ SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Column(
widget.channel.displayName, crossAxisAlignment: CrossAxisAlignment.stretch,
style: const TextStyle(fontWeight: FontWeight.bold), 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),
),
],
), ),
), SizedBox(height: 4),
Text( Row(
(widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()), crossAxisAlignment: CrossAxisAlignment.end,
style: const TextStyle(fontSize: 14), children: [
), Expanded(
], child: Text(
), _preformatTitle(lastMessage),
SizedBox(height: 4), style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
Row( ),
crossAxisAlignment: CrossAxisAlignment.end, ),
children: [ Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
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)), ),
],
), ),
], ],
), ),
@ -94,4 +106,14 @@ class _ChannelListItemState extends State<ChannelListItem> {
if (message == null) return '...'; if (message == null) return '...';
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); 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
}
}
} }

View File

@ -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<ChannelViewPage> createState() => _ChannelViewPageState();
}
class _ChannelViewPageState extends State<ChannelViewPage> {
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<AppAuth, String?>((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<Widget> _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<String> 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,
),
),
),
);
}
}

View File

@ -114,7 +114,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
} }
void _onLifecycleResume() { void _onLifecycleResume() {
if (AppSettings().alwaysBackgroundRefreshMessageListOnLifecycleResume) { if (AppSettings().alwaysBackgroundRefreshMessageListOnLifecycleResume && widget.isVisiblePage) {
ApplicationLog.debug('[MessageList::_onLifecycleResume] --> (will background-refresh)'); ApplicationLog.debug('[MessageList::_onLifecycleResume] --> (will background-refresh)');
_backgroundRefresh(false); _backgroundRefresh(false);
} }