subscription list+view
Some checks failed
Build Docker and Deploy / Build Docker Container (push) Successful in 1m8s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 11m19s
Build Docker and Deploy / Deploy to Server (push) Has been skipped

This commit is contained in:
Mike Schwörer 2025-04-18 01:45:56 +02:00
parent 63bc71c405
commit 24cd1692c6
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
14 changed files with 750 additions and 23 deletions

View File

@ -332,11 +332,26 @@ class APIClient {
);
}
static Future<Subscription> getSubscription(TokenSource auth, String subscriptionID) async {
return await _request(
name: 'getSubscription',
method: 'GET',
relURL: 'users/${auth.getUserID()}/subscriptions/${subscriptionID}',
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
return await _request(
name: 'getSubscriptionList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/subscriptions',
query: {
'direction': ['both'],
'confirmation': ['all'],
'external': ['all'],
},
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
authToken: auth.getToken(),
);

View File

@ -6,6 +6,7 @@ import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.da
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class FilterModalChannel extends StatefulWidget {
@override
@ -83,7 +84,7 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
}
void onOkay() {
Navigator.of(context).pop();
Navi.popDialog(context);
final chiplets = _selectedEntries
.map((e) => MessageFilterChiplet(

View File

@ -6,6 +6,7 @@ import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.da
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class FilterModalKeytoken extends StatefulWidget {
@override
@ -83,7 +84,7 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
}
void onOkay() {
Navigator.of(context).pop();
Navi.popDialog(context);
final chiplets = _selectedEntries
.map((e) => MessageFilterChiplet(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class FilterModalPriority extends StatefulWidget {
@override
@ -58,7 +59,7 @@ class _FilterModalPriorityState extends State<FilterModalPriority> {
}
void onOkay() {
Navigator.of(context).pop();
Navi.popDialog(context);
final chiplets = _selectedEntries.map((e) => MessageFilterChiplet(label: _texts[e]?.$2 ?? '???', value: e, type: MessageFilterChipletType.priority)).toList();

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class FilterModalSearchPlain extends StatefulWidget {
@override
@ -44,7 +45,7 @@ class _FilterModalSearchPlainState extends State<FilterModalSearchPlain> {
}
void _onOkay() {
Navigator.of(context).pop();
Navi.popDialog(context);
List<MessageFilterChiplet> chiplets = [];
if (_controller.text.isNotEmpty) {

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class FilterModalTime extends StatefulWidget {
@override
@ -36,7 +37,7 @@ class _FilterModalTimeState extends State<FilterModalTime> {
}
void onOkay() {
Navigator.of(context).pop();
Navi.popDialog(context);
//TODO
}

View File

@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/pages/client_list/client_list.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list.dart';
import 'package:simplecloudnotifier/pages/sender_list/sender_list.dart';
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
@ -391,19 +392,11 @@ class _AccountRootPageState extends State<AccountRootPage> {
List<Widget> _buildCards(BuildContext context, User user) {
return [
_buildNumberCard(context, 'Subscription', 's', futureSubscriptionCount, () {/*TODO*/}),
_buildNumberCard(context, 'Client', 's', futureClientCount, () {
Navi.push(context, () => ClientListPage());
}),
_buildNumberCard(context, 'Key', 's', futureKeyCount, () {
Navi.push(context, () => KeyTokenListPage());
}),
_buildNumberCard(context, 'Channel', 's', futureChannelAllCount, () {
Navi.push(context, () => ChannelListExtendedPage());
}),
_buildNumberCard(context, 'Sender', '', futureSenderNamesCount, () {
Navi.push(context, () => SenderListPage());
}),
_buildNumberCard(context, 'Subscription', 's', futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
_buildNumberCard(context, 'Client', 's', futureClientCount, () => Navi.push(context, () => ClientListPage())),
_buildNumberCard(context, 'Key', 's', futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
_buildNumberCard(context, 'Channel', 's', futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
_buildNumberCard(context, 'Sender', '', futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),

View File

@ -142,22 +142,22 @@ class _ChannelListItemState extends State<ChannelListItem> {
if (widget.subscription == null && widget.channel.ownerUserID == acc.userID) {
// not-subscribed (own channel)
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
result = GestureDetector(onTap: () => _subscribe(), child: result);
return result;
} else if (widget.subscription == null) {
// not-subscribed (foreign channel)
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
return result;
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
// inactive (own channel)
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
return result;
} else {
// inactive (foreign channel)
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
return result;
}

View File

@ -10,6 +10,7 @@ import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
@ -199,6 +200,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (own)',
values: [_formatSubscriptionStatus(this.subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, null, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
),
_buildForeignSubscriptions(context),
_buildOwnerCard(context, true),
@ -228,6 +230,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, null, _deactivate)],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
);
} else if (subscription != null && subscription!.confirmed && !subscription!.active) {
subCard = UI.metaCard(
@ -236,6 +239,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really (permantenly) delete your subscription to this channel?')), (FontAwesomeIcons.solidSquareRss, null, _activate)],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
);
} else if (subscription != null && !subscription!.confirmed) {
subCard = UI.metaCard(
@ -244,6 +248,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really withdraw your subscription-request to this channel?'))],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
);
} else if (subscription == null) {
subCard = UI.metaCard(
@ -252,6 +257,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
);
} else {
subCard = UI.metaCard(
@ -259,6 +265,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
);
}
@ -314,6 +321,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')',
values: [_formatSubscriptionStatus(sub)],
iconActions: _getForeignIncomingSubActions(sub),
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
),
],
);

View File

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.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/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
class SubscriptionListPage extends StatefulWidget {
const SubscriptionListPage({super.key});
@override
State<SubscriptionListPage> createState() => _SubscriptionListPageState();
}
class _SubscriptionListPageState extends State<SubscriptionListPage> {
final PagingController<int, Subscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
final userCache = Map<String, UserPreview>();
final channelCache = Map<String, ChannelPreview>();
@override
void initState() {
super.initState();
for (var v in SCNDataCache().getChannelMap().entries) channelCache[v.key] = v.value.toPreview(null);
_pagingController.addPageRequestListener(_fetchPage);
_pagingController.refresh();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
void dispose() {
ApplicationLog.debug('SubscriptionListPage::dispose');
_pagingController.dispose();
super.dispose();
}
Future<void> _fetchPage(int pageKey) async {
final acc = Provider.of<AppAuth>(context, listen: false);
ApplicationLog.debug('Start SubscriptionListPage::_pagingController::_fetchPage [ ${pageKey} ]');
if (!acc.isAuth()) {
_pagingController.error = 'Not logged in';
return;
}
try {
final items = (await APIClient.getSubscriptionList(acc)).toList();
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
var promises = Map<String, Future<UserPreview>>();
for (var item in items) {
if (userCache[item.subscriberUserID] == null && !promises.containsKey(item.subscriberUserID)) {
promises[item.subscriberUserID] = APIClient.getUserPreview(acc, item.subscriberUserID).then((p) => userCache[p.userID] = p);
}
if (userCache[item.channelOwnerUserID] == null && !promises.containsKey(item.channelOwnerUserID)) {
promises[item.channelOwnerUserID] = APIClient.getUserPreview(acc, item.channelOwnerUserID).then((p) => userCache[p.userID] = p);
}
if (channelCache[item.channelID] == null && !promises.containsKey(item.channelID)) {
channelCache[item.channelID] = await APIClient.getChannelPreview(acc, item.channelID).then((p) => channelCache[p.channelID] = p);
}
}
await Future.wait(promises.values);
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
} catch (exc, trace) {
_pagingController.error = exc.toString();
ApplicationLog.error('Failed to list subscriptions: ' + exc.toString(), trace: trace);
}
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: "Subscriptions",
showSearch: false,
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, Subscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Subscription>(
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh),
),
),
),
),
);
}
void fullRefresh() {
ApplicationLog.debug('SubscriptionListPage::fullRefresh');
_pagingController.refresh();
}
}

View File

@ -0,0 +1,116 @@
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/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
enum SubscriptionListItemMode {
Messages,
Extended,
}
class SubscriptionListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const SubscriptionListItem({
required this.item,
required this.userCache,
required this.channelCache,
required this.needsReload,
super.key,
});
final Subscription item;
final Map<String, UserPreview> userCache;
final Map<String, ChannelPreview> channelCache;
final void Function()? needsReload;
@override
Widget build(BuildContext context) {
final channelOwner = userCache[item.channelOwnerUserID];
final subscriber = userCache[item.subscriberUserID];
final channel = channelCache[item.channelID];
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
onTap: () {
Navi.push(context, () => SubscriptionViewPage(subscriptionID: item.subscriptionID, preloadedData: (item, channelOwner, subscriber, channel), needsReload: this.needsReload));
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Icon(FontAwesomeIcons.solidDiagramSubtask, color: Theme.of(context).colorScheme.outline, size: 32),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
subscriber?.username ?? item.subscriberUserID,
style: const TextStyle(),
),
SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"@" + (channel?.displayName ?? item.channelID),
style: const TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(width: 10),
Text(
"(" + (channelOwner?.username ?? item.channelOwnerUserID) + ")",
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
],
),
),
SizedBox(width: 4),
Padding(
padding: const EdgeInsets.all(8),
child: _buildIcon(context),
),
],
),
),
),
);
}
Widget _buildIcon(BuildContext context) {
final acc = Provider.of<AppAuth>(context, listen: false);
final colorFull = Theme.of(context).colorScheme.onPrimaryContainer;
final colorHalf = Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(75);
final isOwned = item.channelOwnerUserID == acc.userID && item.subscriberUserID == acc.userID;
final isIncoming = item.channelOwnerUserID == acc.userID && item.subscriberUserID != acc.userID;
final isOutgoing = item.channelOwnerUserID != acc.userID && item.subscriberUserID == acc.userID;
if (isOutgoing && !item.confirmed) return Icon(FontAwesomeIcons.solidSquareEnvelope, color: colorHalf, size: 24);
if (isOutgoing && !item.active) return Icon(FontAwesomeIcons.solidSquareShareNodes, color: colorHalf, size: 24);
if (isOutgoing && item.active) return Icon(FontAwesomeIcons.solidSquareShareNodes, color: colorFull, size: 24);
if (isIncoming && !item.confirmed) return Icon(FontAwesomeIcons.solidSquareQuestion, color: colorHalf, size: 24);
if (isIncoming && !item.active) return Icon(FontAwesomeIcons.solidSquareCheck, color: colorHalf, size: 24);
if (isIncoming && item.active) return Icon(FontAwesomeIcons.solidSquareCheck, color: colorFull, size: 24);
if (isOwned && !item.confirmed) return Icon(FontAwesomeIcons.solidSquare, color: colorHalf, size: 24); // should not be possible
if (isOwned && !item.active) return Icon(FontAwesomeIcons.solidSquareRss, color: colorHalf, size: 24);
if (isOwned && item.active) return Icon(FontAwesomeIcons.solidSquareRss, color: colorFull, size: 24);
return SizedBox(width: 24, height: 24); // should also not be possible
}
}

View File

@ -0,0 +1,468 @@
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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:provider/provider.dart';
class SubscriptionViewPage extends StatefulWidget {
const SubscriptionViewPage({
required this.subscriptionID,
required this.preloadedData,
required this.needsReload,
super.key,
});
final String subscriptionID;
final (Subscription?, UserPreview?, UserPreview?, ChannelPreview?)? preloadedData;
final void Function()? needsReload;
@override
State<SubscriptionViewPage> createState() => _SubscriptionViewPageState();
}
enum EditState { none, editing, saving }
enum SubscriptionViewPageInitState { loading, okay, error }
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
late ImmediateFuture<UserPreview> _futureChannelOwner;
late ImmediateFuture<UserPreview> _futureSubscriber;
late ImmediateFuture<ChannelPreview> _futureChannel;
int _loadingIndeterminateCounter = 0;
Subscription? subscription;
SubscriptionViewPageInitState loadingState = SubscriptionViewPageInitState.loading;
String errorMessage = '';
@override
void initState() {
_initStateAsync(true);
super.initState();
}
Future<void> _initStateAsync(bool usePreload) async {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (widget.preloadedData?.$1 != null && widget.preloadedData!.$1!.subscriptionID == widget.subscriptionID && usePreload) {
subscription = widget.preloadedData!.$1!;
} else {
try {
var r = await APIClient.getSubscription(userAcc, widget.subscriptionID);
setState(() {
subscription = r;
});
} catch (exc, trace) {
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to load data');
this.errorMessage = 'Failed to load data: ' + exc.toString();
this.loadingState = SubscriptionViewPageInitState.error;
return;
}
}
setState(() {
this.loadingState = SubscriptionViewPageInitState.okay;
assert(subscription != null);
if (widget.preloadedData?.$2 != null && widget.preloadedData!.$2!.userID == this.subscription!.channelOwnerUserID && usePreload) {
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$2!);
} else if (widget.preloadedData?.$3 != null && widget.preloadedData!.$3!.userID == this.subscription!.channelOwnerUserID && usePreload) {
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$3!);
} else if (this.subscription!.channelOwnerUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) {
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
} else {
_futureChannelOwner = ImmediateFuture<UserPreview>.ofFuture(_getUserPreview(userAcc, this.subscription!.channelOwnerUserID));
}
} else {
_futureChannelOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.subscription!.channelOwnerUserID));
}
if (widget.preloadedData?.$2 != null && widget.preloadedData!.$2!.userID == this.subscription!.subscriberUserID && usePreload) {
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$2!);
} else if (widget.preloadedData?.$3 != null && widget.preloadedData!.$3!.userID == this.subscription!.subscriberUserID && usePreload) {
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$3!);
} else if (this.subscription!.subscriberUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) {
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
} else {
_futureSubscriber = ImmediateFuture<UserPreview>.ofFuture(_getUserPreview(userAcc, this.subscription!.subscriberUserID));
}
} else {
_futureSubscriber = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.subscription!.subscriberUserID));
}
if (widget.preloadedData?.$4 != null && widget.preloadedData!.$4!.channelID == this.subscription!.channelID && usePreload) {
_futureChannel = ImmediateFuture<ChannelPreview>.ofValue(widget.preloadedData!.$4!);
} else {
_futureChannel = ImmediateFuture<ChannelPreview>.ofFuture(APIClient.getChannelPreview(userAcc, this.subscription!.channelID));
}
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final userAcc = Provider.of<AppAuth>(context, listen: false);
Widget child;
if (loadingState == SubscriptionViewPageInitState.loading) {
child = Center(child: CircularProgressIndicator());
} else if (loadingState == SubscriptionViewPageInitState.error) {
child = Center(child: Text('Error: ' + errorMessage)); //TODO better error
} else if (loadingState == SubscriptionViewPageInitState.okay) {
if (subscription!.channelOwnerUserID == userAcc.userID && subscription!.subscriberUserID == userAcc.userID) {
child = _buildOwnedSubscriptionView(context, this.subscription!);
} else if (subscription!.channelOwnerUserID == userAcc.userID) {
child = _buildIncomingSubscriptionView(context, this.subscription!);
} else if (subscription!.subscriberUserID == userAcc.userID) {
child = _buildOutgoingSubscriptionView(context, this.subscription!);
} else {
child = Center(child: Text('Error: Invalid subscription state!')); //TODO better error
}
} else {
child = Center(child: Text('Error: page state!')); //TODO better error
}
return SCNScaffold(
title: "Subscription",
showSearch: false,
showShare: false,
child: child,
);
}
Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true),
],
),
),
);
}
Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
if (subscription.confirmed) UI.button(text: "Revoke subscription", onPressed: _unsubscribe, color: Colors.red),
if (!subscription.confirmed) UI.button(text: "Confirm subscription", onPressed: _confirm, color: Colors.green),
if (!subscription.confirmed) UI.button(text: "Deny subscription", onPressed: _unsubscribe, color: Colors.red),
],
),
),
);
}
Widget _buildOutgoingSubscriptionView(BuildContext context, Subscription subscription) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'SubscriptionID',
values: [subscription.subscriptionID],
),
_buildChannelOwnerCard(context, subscription),
_buildSubscriberCard(context, subscription),
_buildChannelCard(context, subscription),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
if (subscription.confirmed && subscription.active) UI.button(text: "Deactivate subscription", onPressed: _deactivate, tonal: true),
if (subscription.confirmed && !subscription.active) UI.button(text: "Activate subscription", onPressed: _activate, tonal: true),
if (subscription.confirmed && !subscription.active) UI.button(text: "Delete subscription", onPressed: () => _unsubscribe(confirm: 'Really (permanently) delete the subscription to this channel?'), color: Colors.red),
if (!subscription.confirmed) UI.button(text: "Cancel subscription request", onPressed: _unsubscribe, tonal: true),
],
),
),
);
}
Widget _buildChannelOwnerCard(BuildContext context, Subscription subscription) {
final userAcc = Provider.of<AppAuth>(context, listen: false);
bool isSelf = subscription.channelOwnerUserID == userAcc.userID;
return FutureBuilder(
future: _futureChannelOwner.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Channel Owner',
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
} else {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Channel Owner',
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : '')],
);
}
},
);
}
Widget _buildSubscriberCard(BuildContext context, Subscription subscription) {
final userAcc = Provider.of<AppAuth>(context, listen: false);
bool isSelf = subscription.subscriberUserID == userAcc.userID;
return FutureBuilder(
future: _futureSubscriber.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Subscriber',
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
} else {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Subscriber',
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : '')],
);
}
},
);
}
Widget _buildChannelCard(BuildContext context, Subscription subscription) {
return FutureBuilder(
future: _futureChannel.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [subscription.channelID, snapshot.data!.displayName],
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
);
} else {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channel',
values: [subscription.channelID, subscription.channelInternalName],
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
);
}
},
);
}
Widget _buildStatusCard(BuildContext context) {
final acc = Provider.of<AppAuth>(context, listen: false);
final item = subscription!;
final isOwned = item.channelOwnerUserID == acc.userID && item.subscriberUserID == acc.userID;
final isIncoming = item.channelOwnerUserID == acc.userID && item.subscriberUserID != acc.userID;
final isOutgoing = item.channelOwnerUserID != acc.userID && item.subscriberUserID == acc.userID;
var status = ['ERROR?'];
if (isOutgoing && !item.confirmed) status = ['Subscription to foreign channel', 'Pending confirmation'];
if (isOutgoing && !item.active) status = ['Subscription to foreign channel', 'Confirmed but inactive'];
if (isOutgoing && item.active) status = ['Subscription to foreign channel', 'Confirmed and active'];
if (isIncoming && !item.confirmed) status = ['External subscription to your channel', 'Pending confirmation'];
if (isIncoming && !item.active) status = ['External subscription to your channel', 'Deactivated by subscriber'];
if (isIncoming && item.active) status = ['External subscription to your channel', 'Confirmed and active'];
if (isOwned && !item.confirmed) status = ['Your own channel', 'ERROR'];
if (isOwned && !item.active) status = ['Your own channel', 'Not subscribed'];
if (isOwned && item.active) status = ['Your own channel', 'Active subscription'];
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInfo,
title: 'Status',
values: status,
);
}
Future<UserPreview> _getUserPreview(AppAuth auth, String uid) async {
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
_incLoadingIndeterminateCounter(1);
final owner = APIClient.getUserPreview(auth, uid);
//await Future.delayed(const Duration(seconds: 10), () {});
return owner;
} finally {
_incLoadingIndeterminateCounter(-1);
}
}
void _incLoadingIndeterminateCounter(int delta) {
setState(() {
_loadingIndeterminateCounter += delta;
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
});
}
void _confirm() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.confirmSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
}
}
void _unsubscribe({String? confirm = null}) async {
final acc = AppAuth();
if (subscription == null) return;
if (confirm != null) {
final r = await UIDialogs.showConfirmDialog(context, confirm, okText: 'Unsubscribe', cancelText: 'Cancel');
if (!r) return;
}
try {
await APIClient.deleteSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
Toaster.success("Success", 'Unsubscribed from channel');
Navi.pop(context);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
}
void _deactivate() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.deactivateSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
}
void _activate() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.activateSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
}
}
}

View File

@ -32,6 +32,10 @@ class Navi {
static void popDialog(BuildContext dialogContext) {
Navigator.pop(dialogContext);
}
static void pop(BuildContext context) {
Navigator.of(context).pop();
}
}
class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {

View File

@ -130,7 +130,10 @@ class UI {
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
FaIcon(icon, size: 18),
ConstrainedBox(
constraints: new BoxConstraints(minWidth: 18.0),
child: Center(child: FaIcon(icon, size: 18)),
),
SizedBox(width: 16),
Expanded(
child: Column(