Channel List/view WIP
This commit is contained in:
parent
7dad61dbbb
commit
e2dbe8866d
@ -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) {
|
||||||
|
@ -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>),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
165
flutter/lib/pages/channel_view/channel_view.dart
Normal file
165
flutter/lib/pages/channel_view/channel_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user