2024-06-25 12:00:34 +02:00
import ' package:flutter/material.dart ' ;
2024-06-26 14:54:34 +02:00
import ' package:flutter/widgets.dart ' ;
2024-06-25 12:00:34 +02:00
import ' package:font_awesome_flutter/font_awesome_flutter.dart ' ;
import ' package:qr_flutter/qr_flutter.dart ' ;
import ' package:share_plus/share_plus.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/app_auth.dart ' ;
import ' package:simplecloudnotifier/state/app_bar_state.dart ' ;
2024-06-26 14:54:34 +02:00
import ' package:simplecloudnotifier/state/application_log.dart ' ;
2024-06-25 20:49:40 +02:00
import ' package:simplecloudnotifier/types/immediate_future.dart ' ;
2024-06-26 14:54:34 +02:00
import ' package:simplecloudnotifier/utils/toaster.dart ' ;
2024-06-25 12:00:34 +02:00
import ' package:simplecloudnotifier/utils/ui.dart ' ;
2024-06-25 20:49:40 +02:00
import ' package:provider/provider.dart ' ;
2024-06-25 12:00:34 +02:00
class ChannelViewPage extends StatefulWidget {
const ChannelViewPage ( {
required this . channel ,
required this . subscription ,
2024-06-26 14:54:34 +02:00
required this . needsReload ,
2024-06-25 12:00:34 +02:00
super . key ,
} ) ;
final Channel channel ;
final Subscription ? subscription ;
2024-06-26 14:54:34 +02:00
final void Function ( ) ? needsReload ;
2024-06-25 12:00:34 +02:00
@ override
State < ChannelViewPage > createState ( ) = > _ChannelViewPageState ( ) ;
}
2024-06-26 14:54:34 +02:00
enum EditState { none , editing , saving }
2024-06-25 12:00:34 +02:00
class _ChannelViewPageState extends State < ChannelViewPage > {
2024-06-25 20:49:40 +02:00
late ImmediateFuture < String ? > _futureSubscribeKey ;
late ImmediateFuture < List < Subscription > > _futureSubscriptions ;
late ImmediateFuture < UserPreview > _futureOwner ;
2024-06-26 14:54:34 +02:00
final TextEditingController _ctrlDisplayName = TextEditingController ( ) ;
final TextEditingController _ctrlDescriptionName = TextEditingController ( ) ;
2024-06-25 20:49:40 +02:00
int _loadingIndeterminateCounter = 0 ;
2024-06-25 12:00:34 +02:00
2024-06-26 14:54:34 +02:00
EditState _editDisplayName = EditState . none ;
String ? _displayNameOverride = null ;
EditState _editDescriptionName = EditState . none ;
String ? _descriptionNameOverride = null ;
2024-06-25 12:00:34 +02:00
@ override
void initState ( ) {
2024-06-25 20:49:40 +02:00
final userAcc = Provider . of < AppAuth > ( context , listen: false ) ;
if ( widget . channel . ownerUserID = = userAcc . userID ) {
if ( widget . channel . subscribeKey ! = null ) {
_futureSubscribeKey = ImmediateFuture < String ? > . ofValue ( widget . channel . subscribeKey ) ;
} else {
_futureSubscribeKey = ImmediateFuture < String ? > . ofFuture ( _getSubScribeKey ( userAcc ) ) ;
}
_futureSubscriptions = ImmediateFuture < List < Subscription > > . ofFuture ( _listSubscriptions ( userAcc ) ) ;
} else {
_futureSubscribeKey = ImmediateFuture < String ? > . ofValue ( null ) ;
_futureSubscriptions = ImmediateFuture < List < Subscription > > . ofValue ( [ ] ) ;
}
if ( widget . channel . ownerUserID = = userAcc . userID ) {
var cacheUser = userAcc . getUserOrNull ( ) ;
if ( cacheUser ! = null ) {
_futureOwner = ImmediateFuture < UserPreview > . ofValue ( cacheUser . toPreview ( ) ) ;
} else {
_futureOwner = ImmediateFuture < UserPreview > . ofFuture ( _getOwner ( userAcc ) ) ;
}
} else {
_futureOwner = ImmediateFuture < UserPreview > . ofFuture ( APIClient . getUserPreview ( userAcc , widget . channel . ownerUserID ) ) ;
}
2024-06-25 12:00:34 +02:00
super . initState ( ) ;
}
@ override
void dispose ( ) {
2024-06-26 14:54:34 +02:00
_ctrlDisplayName . dispose ( ) ;
_ctrlDescriptionName . dispose ( ) ;
2024-06-25 12:00:34 +02:00
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
return SCNScaffold (
title: ' Channel ' ,
showSearch: false ,
showShare: false ,
child: _buildChannelView ( context ) ,
) ;
}
Widget _buildChannelView ( BuildContext context ) {
final userAccUserID = context . select < AppAuth , String ? > ( ( v ) = > v . userID ) ;
2024-06-25 20:49:40 +02:00
final isOwned = ( widget . channel . ownerUserID = = userAccUserID ) ;
final isSubscribed = ( widget . subscription ! = null & & widget . subscription ! . confirmed ) ;
2024-06-25 12:00:34 +02:00
return SingleChildScrollView (
child: Padding (
padding: const EdgeInsets . fromLTRB ( 24 , 16 , 24 , 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
_buildQRCode ( context ) ,
SizedBox ( height: 8 ) ,
2024-06-25 20:49:40 +02:00
UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidIdCardClip ,
title: ' ChannelID ' ,
values: [ widget . channel . channelID ] ,
) ,
UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidInputNumeric ,
title: ' InternalName ' ,
values: [ widget . channel . internalName ] ,
) ,
2024-06-26 14:54:34 +02:00
_buildDisplayNameCard ( context , isOwned ) ,
_buildDescriptionNameCard ( context , isOwned ) ,
2024-06-25 20:49:40 +02:00
UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidDiagramSubtask ,
title: ' Subscription (own) ' ,
values: [ _formatSubscriptionStatus ( widget . subscription ) ] ,
iconActions: isSubscribed ? [ ( FontAwesomeIcons . solidSquareXmark , _unsubscribe ) ] : [ ( FontAwesomeIcons . solidSquareRss , _subscribe ) ] ,
) ,
_buildForeignSubscriptions ( context ) ,
_buildOwnerCard ( context , isOwned ) ,
UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidEnvelope ,
title: ' Messages ' ,
values: [ widget . channel . messagesSent . toString ( ) ] ,
mainAction: ( ) { /*TODO*/ } ,
) ,
2024-06-25 12:00:34 +02:00
] ,
) ,
) ,
) ;
}
2024-06-25 20:49:40 +02:00
Widget _buildForeignSubscriptions ( BuildContext context ) {
return FutureBuilder (
future: _futureSubscriptions . future ,
builder: ( context , snapshot ) {
if ( snapshot . hasData ) {
return Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
2024-06-25 12:00:34 +02:00
children: [
2024-06-25 20:49:40 +02:00
for ( final sub in snapshot . data ! . where ( ( sub ) = > sub . subscriptionID ! = widget . subscription ? . subscriptionID ) )
UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidDiagramSuccessor ,
title: ' Subscription (other) ' ,
values: [ _formatSubscriptionStatus ( sub ) ] ,
iconActions: _getForignSubActions ( sub ) ,
) ,
2024-06-25 12:00:34 +02:00
] ,
2024-06-25 20:49:40 +02:00
) ;
} else {
return SizedBox ( ) ;
}
} ,
2024-06-25 12:00:34 +02:00
) ;
2024-06-25 20:49:40 +02:00
}
Widget _buildOwnerCard ( BuildContext context , bool isOwned ) {
return FutureBuilder (
future: _futureOwner . future ,
builder: ( context , snapshot ) {
if ( snapshot . hasData ) {
return UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidUser ,
title: ' Owner ' ,
values: [ widget . channel . ownerUserID + ( isOwned ? ' (you) ' : ' ' ) , if ( snapshot . data ? . username ! = null ) snapshot . data ! . username ! ] ,
) ;
} else {
return UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidUser ,
title: ' Owner ' ,
values: [ widget . channel . ownerUserID + ( isOwned ? ' (you) ' : ' ' ) ] ,
) ;
}
} ,
) ;
}
Widget _buildQRCode ( BuildContext context ) {
return FutureBuilder (
future: _futureSubscribeKey . future ,
builder: ( context , snapshot ) {
if ( snapshot . hasData & & snapshot . data ! = null ) {
var text = ' TODO ' + ' \n ' + widget . channel . channelID + ' \n ' + snapshot . data ! ; //TODO deeplink-y (also perhaps just bas64 everything together?)
return GestureDetector (
onTap: ( ) {
2024-06-26 14:54:34 +02:00
Share . share ( text , subject: _displayNameOverride ? ? widget . channel . displayName ) ;
2024-06-25 20:49:40 +02:00
} ,
child: Center (
child: QrImageView (
data: text ,
version: QrVersions . auto ,
size: 300.0 ,
eyeStyle: QrEyeStyle (
eyeShape: QrEyeShape . square ,
color: Theme . of ( context ) . textTheme . bodyLarge ? . color ,
) ,
dataModuleStyle: QrDataModuleStyle (
dataModuleShape: QrDataModuleShape . square ,
color: Theme . of ( context ) . textTheme . bodyLarge ? . color ,
) ,
) ,
) ,
) ;
} else if ( snapshot . hasData & & snapshot . data = = null ) {
return const SizedBox (
width: 300.0 ,
height: 300.0 ,
child: Center ( child: Icon ( FontAwesomeIcons . solidSnake , size: 64 ) ) ,
) ;
} else {
return const SizedBox (
width: 300.0 ,
height: 300.0 ,
child: Center ( child: CircularProgressIndicator ( ) ) ,
) ;
}
} ,
) ;
}
2024-06-26 14:54:34 +02:00
Widget _buildDisplayNameCard ( BuildContext context , bool isOwned ) {
if ( _editDisplayName = = EditState . editing ) {
return Padding (
padding: EdgeInsets . symmetric ( vertical: 4 , horizontal: 0 ) ,
child: UI . box (
context: context ,
padding: EdgeInsets . fromLTRB ( 16 , 2 , 4 , 2 ) ,
child: Row (
children: [
Container ( child: Center ( child: FaIcon ( FontAwesomeIcons . solidInputText , size: 18 ) ) , height: 43 ) ,
SizedBox ( width: 16 ) ,
Expanded (
child: TextField (
autofocus: true ,
controller: _ctrlDisplayName ,
decoration: new InputDecoration . collapsed ( hintText: ' DisplayName ' ) ,
) ,
) ,
SizedBox ( width: 12 ) ,
SizedBox ( width: 4 ) ,
IconButton ( icon: FaIcon ( FontAwesomeIcons . solidFloppyDisk ) , onPressed: _saveDisplayName ) ,
] ,
) ,
) ,
) ;
} else if ( _editDisplayName = = EditState . none ) {
return UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidInputText ,
title: ' DisplayName ' ,
values: [ _displayNameOverride ? ? widget . channel . displayName ] ,
iconActions: isOwned ? [ ( FontAwesomeIcons . penToSquare , _showEditDisplayName ) ] : [ ] ,
) ;
} else if ( _editDisplayName = = EditState . saving ) {
return Padding (
padding: EdgeInsets . symmetric ( vertical: 4 , horizontal: 0 ) ,
child: UI . box (
context: context ,
padding: EdgeInsets . fromLTRB ( 16 , 2 , 4 , 2 ) ,
child: Row (
children: [
Container ( child: Center ( child: FaIcon ( FontAwesomeIcons . solidInputText , size: 18 ) ) , height: 43 ) ,
SizedBox ( width: 16 ) ,
Expanded ( child: SizedBox ( ) ) ,
SizedBox ( width: 12 ) ,
SizedBox ( width: 4 ) ,
Padding ( padding: const EdgeInsets . all ( 8.0 ) , child: SizedBox ( width: 18 , height: 18 , child: CircularProgressIndicator ( ) ) ) ,
] ,
) ,
) ,
) ;
} else {
throw ' Invalid EditDisplayNameState: $ _editDisplayName ' ;
}
}
Widget _buildDescriptionNameCard ( BuildContext context , bool isOwned ) {
if ( _editDescriptionName = = EditState . editing ) {
return Padding (
padding: EdgeInsets . symmetric ( vertical: 4 , horizontal: 0 ) ,
child: UI . box (
context: context ,
padding: EdgeInsets . fromLTRB ( 16 , 2 , 4 , 2 ) ,
child: Row (
children: [
Container ( child: Center ( child: FaIcon ( FontAwesomeIcons . solidInputPipe , size: 18 ) ) , height: 43 ) ,
SizedBox ( width: 16 ) ,
Expanded (
child: TextField (
autofocus: true ,
controller: _ctrlDescriptionName ,
decoration: new InputDecoration . collapsed ( hintText: ' Description ' ) ,
) ,
) ,
SizedBox ( width: 12 ) ,
SizedBox ( width: 4 ) ,
IconButton ( icon: FaIcon ( FontAwesomeIcons . solidFloppyDisk ) , onPressed: _saveDescriptionName ) ,
] ,
) ,
) ,
) ;
} else if ( _editDescriptionName = = EditState . none ) {
return UI . metaCard (
context: context ,
icon: FontAwesomeIcons . solidInputPipe ,
title: ' Description ' ,
values: [ _descriptionNameOverride ? ? widget . channel . descriptionName ? ? ' ' ] ,
iconActions: isOwned ? [ ( FontAwesomeIcons . penToSquare , _showEditDescriptionName ) ] : [ ] ,
) ;
} else if ( _editDescriptionName = = EditState . saving ) {
return Padding (
padding: EdgeInsets . symmetric ( vertical: 4 , horizontal: 0 ) ,
child: UI . box (
context: context ,
padding: EdgeInsets . fromLTRB ( 16 , 2 , 4 , 2 ) ,
child: Row (
children: [
Container ( child: Center ( child: FaIcon ( FontAwesomeIcons . solidInputPipe , size: 18 ) ) , height: 43 ) ,
SizedBox ( width: 16 ) ,
Expanded ( child: SizedBox ( ) ) ,
SizedBox ( width: 12 ) ,
SizedBox ( width: 4 ) ,
Padding ( padding: const EdgeInsets . all ( 8.0 ) , child: SizedBox ( width: 18 , height: 18 , child: CircularProgressIndicator ( ) ) ) ,
] ,
) ,
) ,
) ;
} else {
throw ' Invalid EditDescriptionNameState: $ _editDescriptionName ' ;
}
2024-06-25 20:49:40 +02:00
}
void _subscribe ( ) {
//TODO
}
void _unsubscribe ( ) {
//TODO
}
2024-06-25 12:00:34 +02:00
2024-06-26 14:54:34 +02:00
void _showEditDisplayName ( ) {
setState ( ( ) {
_ctrlDisplayName . text = _displayNameOverride ? ? widget . channel . displayName ;
_editDisplayName = EditState . editing ;
if ( _editDescriptionName = = EditState . editing ) _editDescriptionName = EditState . none ;
} ) ;
}
void _saveDisplayName ( ) async {
final userAcc = Provider . of < AppAuth > ( context , listen: false ) ;
final newName = _ctrlDisplayName . text ;
try {
setState ( ( ) {
_editDisplayName = EditState . saving ;
} ) ;
final newChannel = await APIClient . updateChannel ( userAcc , widget . channel . channelID , displayName: newName ) ;
setState ( ( ) {
_editDisplayName = EditState . none ;
_displayNameOverride = newChannel . channel . displayName ;
} ) ;
widget . needsReload ? . call ( ) ;
} catch ( exc , trace ) {
ApplicationLog . error ( ' Failed to save DisplayName: ' + exc . toString ( ) , trace: trace ) ;
Toaster . error ( " Error " , ' Failed to save DisplayName ' ) ;
}
}
void _showEditDescriptionName ( ) {
setState ( ( ) {
_ctrlDescriptionName . text = _descriptionNameOverride ? ? widget . channel . descriptionName ? ? ' ' ;
_editDescriptionName = EditState . editing ;
if ( _editDisplayName = = EditState . editing ) _editDisplayName = EditState . none ;
} ) ;
}
void _saveDescriptionName ( ) async {
final userAcc = Provider . of < AppAuth > ( context , listen: false ) ;
final newName = _ctrlDescriptionName . text ;
try {
setState ( ( ) {
_editDescriptionName = EditState . saving ;
} ) ;
final newChannel = await APIClient . updateChannel ( userAcc , widget . channel . channelID , descriptionName: newName ) ;
setState ( ( ) {
_editDescriptionName = EditState . none ;
_descriptionNameOverride = newChannel . channel . descriptionName ? ? ' ' ;
} ) ;
widget . needsReload ? . call ( ) ;
} catch ( exc , trace ) {
ApplicationLog . error ( ' Failed to save DescriptionName: ' + exc . toString ( ) , trace: trace ) ;
Toaster . error ( " Error " , ' Failed to save DescriptionName ' ) ;
}
}
2024-06-25 20:49:40 +02:00
void _cancelForeignSubscription ( Subscription sub ) {
//TODO
}
void _confirmForeignSubscription ( Subscription sub ) {
//TODO
}
void _denyForeignSubscription ( Subscription sub ) {
//TODO
}
String _formatSubscriptionStatus ( Subscription ? subscription ) {
if ( subscription = = null ) {
return ' Not Subscribed ' ;
} else if ( subscription . confirmed ) {
return ' Subscribed ' ;
2024-06-25 12:00:34 +02:00
} else {
2024-06-25 20:49:40 +02:00
return ' Requested ' ;
}
}
Future < String ? > _getSubScribeKey ( AppAuth auth ) 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 ) ;
var channel = await APIClient . getChannel ( auth , widget . channel . channelID ) ;
//await Future.delayed(const Duration(seconds: 10), () {});
return channel . channel . subscribeKey ;
} finally {
_incLoadingIndeterminateCounter ( - 1 ) ;
}
}
Future < List < Subscription > > _listSubscriptions ( AppAuth auth ) 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 ) ;
var subs = await APIClient . getChannelSubscriptions ( auth , widget . channel . channelID ) ;
//await Future.delayed(const Duration(seconds: 10), () {});
return subs ;
} finally {
_incLoadingIndeterminateCounter ( - 1 ) ;
2024-06-25 12:00:34 +02:00
}
}
2024-06-25 20:49:40 +02:00
Future < UserPreview > _getOwner ( AppAuth auth ) 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 , widget . channel . ownerUserID ) ;
//await Future.delayed(const Duration(seconds: 10), () {});
return owner ;
} finally {
_incLoadingIndeterminateCounter ( - 1 ) ;
}
2024-06-25 12:00:34 +02:00
}
2024-06-25 20:49:40 +02:00
List < ( IconData , void Function ( ) ) > _getForignSubActions ( Subscription sub ) {
if ( sub . confirmed ) {
return [ ( FontAwesomeIcons . solidSquareXmark , ( ) = > _cancelForeignSubscription ( sub ) ) ] ;
} else {
return [
( FontAwesomeIcons . solidSquareCheck , ( ) = > _confirmForeignSubscription ( sub ) ) ,
( FontAwesomeIcons . solidSquareXmark , ( ) = > _denyForeignSubscription ( sub ) ) ,
] ;
2024-06-25 12:00:34 +02:00
}
}
2024-06-25 20:49:40 +02:00
void _incLoadingIndeterminateCounter ( int delta ) {
setState ( ( ) {
_loadingIndeterminateCounter + = delta ;
AppBarState ( ) . setLoadingIndeterminate ( _loadingIndeterminateCounter > 0 ) ;
} ) ;
2024-06-25 12:00:34 +02:00
}
}