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 createState() => _RealmManageScreenState(); } class _RealmManageScreenState extends State { bool _isBusy = false; SnRealm? _editingRealm; Future _fetchRealm() async { final sn = context.read(); 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 _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(); 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 _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(); 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(); 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(Symbols.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), ], ), ), ); } }