edit displayName/descriptionName of channel

This commit is contained in:
Mike Schwörer 2024-06-26 14:54:34 +02:00
parent 1f9b65652d
commit 89d1e0f641
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
3 changed files with 250 additions and 14 deletions

View File

@ -7,6 +7,7 @@ import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/request_log.dart';
@ -211,6 +212,20 @@ class APIClient {
); );
} }
static Future<ChannelWithSubscription> updateChannel(AppAuth auth, String cid, {String? displayName, String? descriptionName}) async {
return await _request(
name: 'updateChannel',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/channels/${cid}',
jsonBody: {
if (displayName != null) 'display_name': displayName,
if (descriptionName != null) 'description_name': descriptionName,
},
fn: ChannelWithSubscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async { static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
return await _request( return await _request(
name: 'getMessageList', name: 'getMessageList',

View File

@ -19,11 +19,13 @@ class ChannelRootPage extends StatefulWidget {
State<ChannelRootPage> createState() => _ChannelRootPageState(); State<ChannelRootPage> createState() => _ChannelRootPageState();
} }
class _ChannelRootPageState extends State<ChannelRootPage> { class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0); final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
bool _isInitialized = false; bool _isInitialized = false;
bool _reloadEnqueued = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -33,10 +35,17 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
if (widget.isVisiblePage && !_isInitialized) _realInitState(); if (widget.isVisiblePage && !_isInitialized) _realInitState();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
}
@override @override
void dispose() { void dispose() {
ApplicationLog.debug('ChannelRootPage::dispose'); ApplicationLog.debug('ChannelRootPage::dispose');
_pagingController.dispose(); _pagingController.dispose();
Navi.modalRouteObserver.unsubscribe(this);
super.dispose(); super.dispose();
} }
@ -53,6 +62,24 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
} }
} }
@override
void didPush() {
// ...
}
@override
void didPopNext() {
if (_reloadEnqueued) {
ApplicationLog.debug('[ChannelList::RouteObserver] --> didPopNext (will background-refresh) (_reloadEnqueued == true)');
() async {
_reloadEnqueued = false;
AppBarState().setLoadingIndeterminate(true);
await Future.delayed(const Duration(milliseconds: 500)); // prevents flutter bug where the whole process crashes ?!?
await _backgroundRefresh();
}();
}
}
void _realInitState() { void _realInitState() {
ApplicationLog.debug('ChannelRootPage::_realInitState'); ApplicationLog.debug('ChannelRootPage::_realInitState');
_pagingController.refresh(); _pagingController.refresh();
@ -100,9 +127,13 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? '')); items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
setState(() {
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null); _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
});
} catch (exc, trace) { } catch (exc, trace) {
setState(() {
_pagingController.error = exc.toString(); _pagingController.error = exc.toString();
});
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
} finally { } finally {
AppBarState().setLoadingIndeterminate(false); AppBarState().setLoadingIndeterminate(false);
@ -122,11 +153,15 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
channel: item.channel, channel: item.channel,
subscription: item.subscription, subscription: item.subscription,
onPressed: () { onPressed: () {
Navi.push(context, () => ChannelViewPage(channel: item.channel, subscription: item.subscription)); Navi.push(context, () => ChannelViewPage(channel: item.channel, subscription: item.subscription, needsReload: _enqueueReload));
}, },
), ),
), ),
), ),
); );
} }
void _enqueueReload() {
_reloadEnqueued = true;
}
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@ -9,7 +10,9 @@ import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,23 +20,37 @@ class ChannelViewPage extends StatefulWidget {
const ChannelViewPage({ const ChannelViewPage({
required this.channel, required this.channel,
required this.subscription, required this.subscription,
required this.needsReload,
super.key, super.key,
}); });
final Channel channel; final Channel channel;
final Subscription? subscription; final Subscription? subscription;
final void Function()? needsReload;
@override @override
State<ChannelViewPage> createState() => _ChannelViewPageState(); State<ChannelViewPage> createState() => _ChannelViewPageState();
} }
enum EditState { none, editing, saving }
class _ChannelViewPageState extends State<ChannelViewPage> { class _ChannelViewPageState extends State<ChannelViewPage> {
late ImmediateFuture<String?> _futureSubscribeKey; late ImmediateFuture<String?> _futureSubscribeKey;
late ImmediateFuture<List<Subscription>> _futureSubscriptions; late ImmediateFuture<List<Subscription>> _futureSubscriptions;
late ImmediateFuture<UserPreview> _futureOwner; late ImmediateFuture<UserPreview> _futureOwner;
final TextEditingController _ctrlDisplayName = TextEditingController();
final TextEditingController _ctrlDescriptionName = TextEditingController();
int _loadingIndeterminateCounter = 0; int _loadingIndeterminateCounter = 0;
EditState _editDisplayName = EditState.none;
String? _displayNameOverride = null;
EditState _editDescriptionName = EditState.none;
String? _descriptionNameOverride = null;
@override @override
void initState() { void initState() {
final userAcc = Provider.of<AppAuth>(context, listen: false); final userAcc = Provider.of<AppAuth>(context, listen: false);
@ -66,6 +83,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
@override @override
void dispose() { void dispose() {
_ctrlDisplayName.dispose();
_ctrlDescriptionName.dispose();
super.dispose(); super.dispose();
} }
@ -105,13 +124,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
title: 'InternalName', title: 'InternalName',
values: [widget.channel.internalName], values: [widget.channel.internalName],
), ),
UI.metaCard( _buildDisplayNameCard(context, isOwned),
context: context, _buildDescriptionNameCard(context, isOwned),
icon: FontAwesomeIcons.solidInputText,
title: 'DisplayName',
values: [widget.channel.displayName],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _rename)] : [],
),
UI.metaCard( UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.solidDiagramSubtask, icon: FontAwesomeIcons.solidDiagramSubtask,
@ -190,7 +204,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
var text = 'TODO' + '\n' + widget.channel.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?) var text = 'TODO' + '\n' + widget.channel.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Share.share(text, subject: widget.channel.displayName); Share.share(text, subject: _displayNameOverride ?? widget.channel.displayName);
}, },
child: Center( child: Center(
child: QrImageView( child: QrImageView(
@ -225,8 +239,116 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
); );
} }
void _rename() { Widget _buildDisplayNameCard(BuildContext context, bool isOwned) {
//TODO 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';
}
} }
void _subscribe() { void _subscribe() {
@ -237,6 +359,70 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
//TODO //TODO
} }
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');
}
}
void _cancelForeignSubscription(Subscription sub) { void _cancelForeignSubscription(Subscription sub) {
//TODO //TODO
} }