subscription list+view
This commit is contained in:
parent
63bc71c405
commit
24cd1692c6
@ -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(),
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
115
flutter/lib/pages/subscription_list/subscription_list.dart
Normal file
115
flutter/lib/pages/subscription_list/subscription_list.dart
Normal 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();
|
||||
}
|
||||
}
|
116
flutter/lib/pages/subscription_list/subscription_list_item.dart
Normal file
116
flutter/lib/pages/subscription_list/subscription_list_item.dart
Normal 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
|
||||
}
|
||||
}
|
468
flutter/lib/pages/subscription_view/subscription_view.dart
Normal file
468
flutter/lib/pages/subscription_view/subscription_view.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>> {
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user