2025-04-18 01:45:56 +02:00
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 ' ;
2025-04-18 14:07:31 +02:00
import ' package:simplecloudnotifier/components/error_display/error_display.dart ' ;
2025-04-18 01:45:56 +02:00
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 ) {
2025-04-18 14:07:31 +02:00
child = ErrorDisplay ( errorMessage: errorMessage ) ;
2025-04-18 01:45:56 +02:00
} 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 {
2025-04-18 14:07:31 +02:00
child = ErrorDisplay ( errorMessage: ' Invalid subscription state! ' ) ;
2025-04-18 01:45:56 +02:00
}
} else {
2025-04-18 14:07:31 +02:00
child = ErrorDisplay ( errorMessage: ' Invalid page state! ' ) ;
2025-04-18 01:45:56 +02:00
}
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 ) ;
}
}
}