475 lines
19 KiB
Dart
475 lines
19 KiB
Dart
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/error_display/error_display.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/app_settings.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> {
|
|
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
|
|
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
|
|
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
|
|
|
|
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 = ErrorDisplay(errorMessage: errorMessage);
|
|
} 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 = ErrorDisplay(errorMessage: 'Invalid subscription state!');
|
|
}
|
|
} else {
|
|
child = ErrorDisplay(errorMessage: 'Invalid page state!');
|
|
}
|
|
|
|
return SCNScaffold(
|
|
title: "Subscription",
|
|
showSearch: false,
|
|
showShare: false,
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) {
|
|
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
|
|
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: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
|
|
),
|
|
_buildStatusCard(context),
|
|
UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) {
|
|
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
|
|
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: [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) {
|
|
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
|
|
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: [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);
|
|
}
|
|
}
|
|
}
|