Realm manage (CRUD)

This commit is contained in:
2024-11-16 13:54:36 +08:00
parent ee2cb0c989
commit 0e208cc320
15 changed files with 1785 additions and 25 deletions

View File

@ -148,20 +148,14 @@ class _AccountPublisherEditScreenState
mimetype: 'image/png',
);
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
await sn.client.put(
'/cgi/id/users/me/$place',
data: {'attachment': attachment.rid},
);
if (!mounted) return;
final ua = context.read<UserProvider>();
await ua.refreshUser();
if (!mounted) return;
context.showSnackbar('accountProfileEditApplied'.tr());
_syncWidget();
switch (place) {
case 'avatar':
_avatar = attachment.rid;
break;
case 'banner':
_banner = attachment.rid;
break;
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -286,7 +280,7 @@ class _AccountPublisherEditScreenState
],
)
],
).padding(horizontal: 16, vertical: 12),
).padding(horizontal: 24, vertical: 12),
),
);
}

View File

@ -1,10 +1,40 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
class ChatScreen extends StatelessWidget {
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
Future<void> _fetchChannels({scope = 'global', direct = false}) async {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/im/channels/$scope/me/available',
queryParameters: {
'direct': direct,
},
);
}
@override
Widget build(BuildContext context) {
return const Placeholder();
return Scaffold(
appBar: AppBar(
title: Text('screenChat').tr(),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.chat_add_on),
onPressed: () {
GoRouter.of(context).pushNamed('chatManage');
},
),
);
}
}

View File

@ -0,0 +1,139 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:uuid/uuid.dart';
class ChatManageScreen extends StatefulWidget {
final String? editingChannelAlias;
const ChatManageScreen({super.key, this.editingChannelAlias});
@override
State<ChatManageScreen> createState() => _ChatManageScreenState();
}
class _ChatManageScreenState extends State<ChatManageScreen> {
bool _isBusy = false;
final _aliasController = TextEditingController();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
Future<void> _performAction() async {
final uuid = const Uuid();
final sn = context.read<SnNetworkProvider>();
setState(() => _isBusy = true);
// TODO Add realm support
// final scope = widget.realm != null ? widget.realm!.alias : 'global';
final scope = 'global';
final payload = {
'alias': _aliasController.text.isNotEmpty
? _aliasController.text.toLowerCase()
: uuid.v4().replaceAll('-', '').substring(0, 12),
'name': _nameController.text,
'description': _descriptionController.text,
};
try {
final resp = await sn.client.request(
widget.editingChannelAlias != null
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
: '/cgi/im/channels/$scope',
data: payload,
options: Options(
method: widget.editingChannelAlias != null ? 'PUT' : 'POST',
),
);
log(jsonEncode(resp.data));
// ignore: use_build_context_synchronously
if (context.mounted) Navigator.pop(context, resp.data);
} catch (err) {
// ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err);
}
setState(() => _isBusy = false);
}
@override
void dispose() {
super.dispose();
_aliasController.dispose();
_nameController.dispose();
_descriptionController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: widget.editingChannelAlias != null
? Text('screenChatManage').tr()
: Text('screenChatNew').tr(),
),
body: SingleChildScrollView(
child: Column(
children: [
LoadingIndicator(isActive: _isBusy),
const Gap(24),
TextField(
controller: _aliasController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldChatAlias'.tr(),
helperText: 'fieldChatAliasHint'.tr(),
helperMaxLines: 2,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _nameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldChatName'.tr(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _descriptionController,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldChatDescription'.tr(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton.icon(
onPressed: _isBusy ? null : _performAction,
icon: const Icon(Symbols.save),
label: Text('apply').tr(),
),
],
),
],
).padding(horizontal: 24),
),
);
}
}

220
lib/screens/realm.dart Normal file
View File

@ -0,0 +1,220 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/universal_image.dart';
class RealmScreen extends StatefulWidget {
const RealmScreen({super.key});
@override
State<RealmScreen> createState() => _RealmScreenState();
}
class _RealmScreenState extends State<RealmScreen> {
bool _isBusy = false;
bool _isCompactView = false;
List<SnRealm>? _realms;
Future<void> _fetchRealms() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/realms/me/available');
_realms = List<SnRealm>.from(
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
);
} catch (err) {
if (mounted) context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _deleteRealm(SnRealm realm) async {
final confirm = await context.showConfirmDialog(
'realmDelete'.tr(args: ['#${realm.alias}']),
'realmDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
setState(() => _isBusy = true);
try {
await sn.client.delete('/cgi/id/realms/${realm.alias}');
if (!mounted) return;
context.showSnackbar('realmDeleted'.tr(args: ['#${realm.alias}']));
_fetchRealms();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchRealms();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return Scaffold(
appBar: AppBar(
title: Text('screenRealm').tr(),
actions: [
IconButton(
icon: !_isCompactView
? const Icon(Icons.view_list)
: const Icon(Icons.view_module),
onPressed: () {
setState(() => _isCompactView = !_isCompactView);
},
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.group_add),
onPressed: () {
GoRouter.of(context).pushNamed('realmManage');
},
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: RefreshIndicator(
onRefresh: _fetchRealms,
child: ListView.builder(
itemCount: _realms?.length ?? 0,
itemBuilder: (context, idx) {
final realm = _realms![idx];
if (_isCompactView) {
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: realm.avatar,
fallbackWidget: const Icon(Symbols.group, size: 20),
),
title: Text(realm.name),
subtitle: Text(
realm.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
);
}
return Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context)
.colorScheme
.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget:
const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(
Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(
Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {},
),
);
},
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,312 @@
import 'dart:io';
import 'dart:ui';
import 'package:croppy/croppy.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' show basename;
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
class RealmManageScreen extends StatefulWidget {
final String? editingRealmAlias;
const RealmManageScreen({super.key, this.editingRealmAlias});
@override
State<RealmManageScreen> createState() => _RealmManageScreenState();
}
class _RealmManageScreenState extends State<RealmManageScreen> {
bool _isBusy = false;
SnRealm? _editingRealm;
Future<void> _fetchRealm() async {
final sn = context.read<SnNetworkProvider>();
setState(() => _isBusy = true);
try {
final resp =
await sn.client.get('/cgi/id/realms/${widget.editingRealmAlias}');
final out = SnRealm.fromJson(resp.data);
_editingRealm = out;
_avatar = out.avatar;
_banner = out.banner;
_aliasController.text = out.alias;
_nameController.text = out.name;
_descriptionController.text = out.description;
} catch (err) {
// ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
String? _avatar;
String? _banner;
final _aliasController = TextEditingController();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
final _imagePicker = ImagePicker();
Future<void> _updateImage(String place) async {
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
if (!mounted) return;
final ImageProvider imageProvider =
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
final aspectRatios = place == 'banner'
? [CropAspectRatio(width: 16, height: 7)]
: [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
)
: await showMaterialImageCropper(
// ignore: use_build_context_synchronously
context,
allowedAspectRatios: aspectRatios,
imageProvider: imageProvider,
);
if (result == null) return;
if (!mounted) return;
final attach = context.read<SnAttachmentProvider>();
setState(() => _isBusy = true);
final rawBytes =
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
try {
final attachment = await attach.directUploadOne(
rawBytes,
basename(image.path),
'avatar',
null,
mimetype: 'image/png',
);
switch (place) {
case 'avatar':
_avatar = attachment.rid;
break;
case 'banner':
_banner = attachment.rid;
break;
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _performAction() async {
final uuid = const Uuid();
final payload = {
'alias': _aliasController.text.isNotEmpty
? _aliasController.text.toLowerCase()
: uuid.v4().replaceAll('-', '').substring(0, 12),
'name': _nameController.text,
'description': _descriptionController.text,
'avatar': _avatar,
'banner': _banner,
};
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.request(
widget.editingRealmAlias != null
? '/cgi/id/realms/${widget.editingRealmAlias}'
: '/cgi/id/realms',
data: payload,
options: Options(
method: widget.editingRealmAlias != null ? 'PUT' : 'POST',
),
);
final out = SnRealm.fromJson(resp.data);
// ignore: use_build_context_synchronously
if (context.mounted) Navigator.pop(context, out);
} catch (err) {
// ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
if (widget.editingRealmAlias != null) _fetchRealm();
}
@override
void dispose() {
_aliasController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return Scaffold(
appBar: AppBar(
title: widget.editingRealmAlias != null
? Text('screenRealmManage').tr()
: Text('screenRealmNew').tr(),
),
body: SingleChildScrollView(
child: Column(
children: [
LoadingIndicator(isActive: _isBusy),
if (_editingRealm != null)
MaterialBanner(
leading: const Icon(Icons.edit),
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text(
'realmEditingNotice'.tr(args: ['#${_editingRealm!.alias}']),
),
actions: [
TextButton(
child: Text('cancel').tr(),
onPressed: () {
Navigator.pop(context);
},
),
],
),
const Gap(24),
Stack(
clipBehavior: Clip.none,
children: [
Material(
elevation: 0,
child: InkWell(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
),
),
onTap: () {
_updateImage('banner');
},
),
),
Positioned(
bottom: -28,
left: 16,
child: Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(40)),
child: InkWell(
child: AccountImage(
content: _avatar,
radius: 40,
fallbackWidget: const Icon(Symbols.group, size: 40),
),
onTap: () {
_updateImage('avatar');
},
),
),
),
],
).padding(horizontal: 24),
const Gap(8 + 28),
Column(
children: [
TextField(
controller: _aliasController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldRealmAlias'.tr(),
helperText: 'fieldRealmAliasHint'.tr(),
helperMaxLines: 2,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _nameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldRealmName'.tr(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _descriptionController,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldRealmDescription'.tr(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton.icon(
onPressed: _isBusy ? null : _performAction,
icon: const Icon(Symbols.save),
label: Text('apply').tr(),
),
],
),
],
).padding(horizontal: 24 + 8),
],
),
),
);
}
}