Mike Schwörer 24cd1692c6
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
subscription list+view
2025-04-18 01:45:56 +02:00

469 lines
18 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/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);
}
}
}