diff --git a/assets/translations/en.json b/assets/translations/en.json index 254d946..537ca11 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -17,6 +17,11 @@ "screenSettings": "Settings", "screenAlbum": "Album", "screenChat": "Chat", + "screenChatManage": "Edit Channel", + "screenChatNew": "New Channel", + "screenRealm": "Realm", + "screenRealmManage": "Edit Realm", + "screenRealmNew": "New Realm", "dialogOkay": "Okay", "dialogCancel": "Cancel", "dialogConfirm": "Confirm", @@ -32,6 +37,7 @@ "next": "Next", "edit": "Edit", "apply": "Apply", + "cancel": "Cancel", "create": "Create", "preview": "Preview", "loading": "Loading...", @@ -133,5 +139,21 @@ "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", "sensitiveContentReveal": "Reveal", "serverConnecting": "Connecting to server...", - "serverDisconnected": "Lost connection from server" + "serverDisconnected": "Lost connection from server", + "fieldChatAlias": "Channel Alias", + "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.", + "fieldChatName": "Name", + "fieldChatDescription": "Description", + "channelEditingNotice": "You are editing channel {}", + "channelDeleted": "Chat channel {} has been deleted." , + "channelDelete": "Delete channel {}", + "channelDeleteDescription": "Are you sure you want to delete this channel? This operation is irreversible, all messages in this channel will be permanently deleted.", + "fieldRealmAlias": "Realm Alias", + "fieldRealmAliasHint": "The unique realm alias within the site, used to represent the realm in URL, leave blank to auto generate. Should be URL-Safe.", + "fieldRealmName": "Name", + "fieldRealmDescription": "Description", + "realmEditingNotice": "You are editing realm {}", + "realmDeleted": "Realm {} has been deleted.", + "realmDelete": "Delete realm {}", + "realmDeleteDescription": "Are you sure you want to delete this realm? This operation is irreversible, all resources (posts, chat channels, publishers, etc) belonging to this realm will be permanently deleted. Be careful and think twice!" } diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 46acd89..9820a3b 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -17,6 +17,11 @@ "screenSettings": "设置", "screenAlbum": "相册", "screenChat": "聊天", + "screenChatManage": "编辑聊天频道", + "screenChatNew": "新建聊天频道", + "screenRealm": "领域", + "screenRealmManage": "编辑领域", + "screenRealmNew": "新建领域", "dialogOkay": "好的", "dialogCancel": "取消", "dialogConfirm": "确认", @@ -33,6 +38,7 @@ "next": "下一步", "edit": "编辑", "apply": "应用", + "cancel": "取消", "create": "创建", "preview": "预览", "delete": "删除", @@ -133,5 +139,21 @@ "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", "sensitiveContentReveal": "显示内容", "serverConnecting": "正在连接服务器…", - "serverDisconnected": "已与服务器断开连接" + "serverDisconnected": "已与服务器断开连接", + "fieldChatAlias": "频道别名", + "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。", + "fieldChatName": "名称", + "fieldChatDescription": "描述", + "channelEditingNotice": "您正在编辑频道 {}", + "channelDeleted": "聊天频道 {} 已被删除" , + "channelDelete": "删除聊天频道 {}", + "channelDeleteDescription": "你确定要删除这个聊天频道吗?该操作不可撤销,其频道内的所有消息将被永久删除。", + "fieldRealmAlias": "领域别名", + "fieldRealmAliasHint": "全站范围内唯一的领域别名,用于在 URL 中表示该领域,留空则自动生成。应遵循 URL-Safe 的原则。", + "fieldRealmName": "名称", + "fieldRealmDescription": "描述", + "realmEditingNotice": "您正在编辑领域 {}", + "realmDeleted": "领域 {} 已被删除" , + "realmDelete": "删除领域 {}", + "realmDeleteDescription": "你确定要删除这个领域吗?该操作不可撤销,其隶属于该领域的所有资源(帖子、聊天频道、发布者、制品等)都将被永久删除。三思而后行!" } diff --git a/lib/main.dart b/lib/main.dart index 4201c0b..685668f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; import 'package:responsive_framework/responsive_framework.dart'; @@ -18,6 +19,8 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + await Hive.initFlutter(); + if (!kReleaseMode) { debugInvertOversizedImages = true; } diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index 027dea5..d5bf27a 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -43,26 +43,32 @@ class NavigationProvider extends ChangeNotifier { screen: 'explore', label: 'screenExplore', ), + AppNavDestination( + icon: Icon(Symbols.chat, weight: 400, opticalSize: 20), + screen: 'chat', + label: 'screenChat', + ), AppNavDestination( icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20), screen: 'account', label: 'screenAccount', ), + AppNavDestination( + icon: Icon(Symbols.group, weight: 400, opticalSize: 20), + screen: 'realm', + label: 'screenRealm', + ), AppNavDestination( icon: Icon(Symbols.album, weight: 400, opticalSize: 20), screen: 'album', label: 'screenAlbum', ), - AppNavDestination( - icon: Icon(Symbols.chat, weight: 400, opticalSize: 20), - screen: 'chat', - label: 'screenChat', - ), ]; static const List kDefaultPinnedDestination = [ 'home', 'explore', - 'account' + 'chat', + 'account', ]; List destinations = []; diff --git a/lib/router.dart b/lib/router.dart index c9ecd95..e7ece8c 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -9,10 +9,13 @@ import 'package:surface/screens/album.dart'; import 'package:surface/screens/auth/login.dart'; import 'package:surface/screens/auth/register.dart'; import 'package:surface/screens/chat.dart'; +import 'package:surface/screens/chat/manage.dart'; import 'package:surface/screens/explore.dart'; import 'package:surface/screens/home.dart'; import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_editor.dart'; +import 'package:surface/screens/realm.dart'; +import 'package:surface/screens/realm/manage.dart'; import 'package:surface/screens/settings.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/navigation/app_background.dart'; @@ -98,6 +101,55 @@ final _appRoutes = [ pageBuilder: (context, state) => NoTransitionPage( child: const ChatScreen(), ), + routes: [ + GoRoute( + path: '/chat/manage', + name: 'chatManage', + pageBuilder: (context, state) => CustomTransitionPage( + child: ChatManageScreen(), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: AppBackground( + isLessOptimization: true, + child: child, + ), + ); + }, + ), + ), + ], + ), + GoRoute( + path: '/realm', + name: 'realm', + pageBuilder: (context, state) => NoTransitionPage( + child: const RealmScreen(), + ), + routes: [ + GoRoute( + path: '/realm/manage', + name: 'realmManage', + pageBuilder: (context, state) => CustomTransitionPage( + child: RealmManageScreen( + editingRealmAlias: state.uri.queryParameters['editing'], + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: AppBackground( + isLessOptimization: true, + child: child, + ), + ); + }, + ), + ), + ], ), GoRoute( path: '/album', diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index fe7a215..06d6e8d 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -148,20 +148,14 @@ class _AccountPublisherEditScreenState mimetype: 'image/png', ); - if (!mounted) return; - final sn = context.read(); - await sn.client.put( - '/cgi/id/users/me/$place', - data: {'attachment': attachment.rid}, - ); - - if (!mounted) return; - final ua = context.read(); - 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), ), ); } diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 437de3f..0c92fe9 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -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 createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + Future _fetchChannels({scope = 'global', direct = false}) async { + final sn = context.read(); + 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'); + }, + ), + ); } } diff --git a/lib/screens/chat/manage.dart b/lib/screens/chat/manage.dart new file mode 100644 index 0000000..bf6fbb4 --- /dev/null +++ b/lib/screens/chat/manage.dart @@ -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 createState() => _ChatManageScreenState(); +} + +class _ChatManageScreenState extends State { + bool _isBusy = false; + + final _aliasController = TextEditingController(); + final _nameController = TextEditingController(); + final _descriptionController = TextEditingController(); + + Future _performAction() async { + final uuid = const Uuid(); + final sn = context.read(); + + 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), + ), + ); + } +} diff --git a/lib/screens/realm.dart b/lib/screens/realm.dart new file mode 100644 index 0000000..cf0753c --- /dev/null +++ b/lib/screens/realm.dart @@ -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 createState() => _RealmScreenState(); +} + +class _RealmScreenState extends State { + bool _isBusy = false; + bool _isCompactView = false; + + List? _realms; + + Future _fetchRealms() async { + setState(() => _isBusy = true); + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/realms/me/available'); + _realms = List.from( + resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], + ); + } catch (err) { + if (mounted) context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + Future _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(); + + 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(); + + 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: () {}, + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/realm/manage.dart b/lib/screens/realm/manage.dart new file mode 100644 index 0000000..6594cfb --- /dev/null +++ b/lib/screens/realm/manage.dart @@ -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 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(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), + ], + ), + ), + ); + } +} diff --git a/lib/types/realm.dart b/lib/types/realm.dart new file mode 100644 index 0000000..2370fe3 --- /dev/null +++ b/lib/types/realm.dart @@ -0,0 +1,46 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:surface/types/account.dart'; + +part 'realm.freezed.dart'; +part 'realm.g.dart'; + +@freezed +class SnRealmMember with _$SnRealmMember { + const factory SnRealmMember({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required int realmId, + required int accountId, + required SnRealm realm, + required SnAccount account, + required int powerLevel, + }) = _SnRealmMember; + + factory SnRealmMember.fromJson(Map json) => + _$SnRealmMemberFromJson(json); +} + +@freezed +class SnRealm with _$SnRealm { + const factory SnRealm({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + required String alias, + required String name, + required String description, + required List? members, + required String? avatar, + required String? banner, + required Map? accessPolicy, + required bool isPublic, + required bool isCommunity, + required int accountId, + }) = _SnRealm; + + factory SnRealm.fromJson(Map json) => + _$SnRealmFromJson(json); +} diff --git a/lib/types/realm.freezed.dart b/lib/types/realm.freezed.dart new file mode 100644 index 0000000..a3b6f9b --- /dev/null +++ b/lib/types/realm.freezed.dart @@ -0,0 +1,812 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'realm.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +SnRealmMember _$SnRealmMemberFromJson(Map json) { + return _SnRealmMember.fromJson(json); +} + +/// @nodoc +mixin _$SnRealmMember { + int get id => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + DateTime? get deletedAt => throw _privateConstructorUsedError; + int get realmId => throw _privateConstructorUsedError; + int get accountId => throw _privateConstructorUsedError; + SnRealm get realm => throw _privateConstructorUsedError; + SnAccount get account => throw _privateConstructorUsedError; + int get powerLevel => throw _privateConstructorUsedError; + + /// Serializes this SnRealmMember to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnRealmMemberCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnRealmMemberCopyWith<$Res> { + factory $SnRealmMemberCopyWith( + SnRealmMember value, $Res Function(SnRealmMember) then) = + _$SnRealmMemberCopyWithImpl<$Res, SnRealmMember>; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + int realmId, + int accountId, + SnRealm realm, + SnAccount account, + int powerLevel}); + + $SnRealmCopyWith<$Res> get realm; + $SnAccountCopyWith<$Res> get account; +} + +/// @nodoc +class _$SnRealmMemberCopyWithImpl<$Res, $Val extends SnRealmMember> + implements $SnRealmMemberCopyWith<$Res> { + _$SnRealmMemberCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? realmId = null, + Object? accountId = null, + Object? realm = null, + Object? account = null, + Object? powerLevel = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + realmId: null == realmId + ? _value.realmId + : realmId // ignore: cast_nullable_to_non_nullable + as int, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + realm: null == realm + ? _value.realm + : realm // ignore: cast_nullable_to_non_nullable + as SnRealm, + account: null == account + ? _value.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount, + powerLevel: null == powerLevel + ? _value.powerLevel + : powerLevel // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnRealmCopyWith<$Res> get realm { + return $SnRealmCopyWith<$Res>(_value.realm, (value) { + return _then(_value.copyWith(realm: value) as $Val); + }); + } + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAccountCopyWith<$Res> get account { + return $SnAccountCopyWith<$Res>(_value.account, (value) { + return _then(_value.copyWith(account: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SnRealmMemberImplCopyWith<$Res> + implements $SnRealmMemberCopyWith<$Res> { + factory _$$SnRealmMemberImplCopyWith( + _$SnRealmMemberImpl value, $Res Function(_$SnRealmMemberImpl) then) = + __$$SnRealmMemberImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + int realmId, + int accountId, + SnRealm realm, + SnAccount account, + int powerLevel}); + + @override + $SnRealmCopyWith<$Res> get realm; + @override + $SnAccountCopyWith<$Res> get account; +} + +/// @nodoc +class __$$SnRealmMemberImplCopyWithImpl<$Res> + extends _$SnRealmMemberCopyWithImpl<$Res, _$SnRealmMemberImpl> + implements _$$SnRealmMemberImplCopyWith<$Res> { + __$$SnRealmMemberImplCopyWithImpl( + _$SnRealmMemberImpl _value, $Res Function(_$SnRealmMemberImpl) _then) + : super(_value, _then); + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? realmId = null, + Object? accountId = null, + Object? realm = null, + Object? account = null, + Object? powerLevel = null, + }) { + return _then(_$SnRealmMemberImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + realmId: null == realmId + ? _value.realmId + : realmId // ignore: cast_nullable_to_non_nullable + as int, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + realm: null == realm + ? _value.realm + : realm // ignore: cast_nullable_to_non_nullable + as SnRealm, + account: null == account + ? _value.account + : account // ignore: cast_nullable_to_non_nullable + as SnAccount, + powerLevel: null == powerLevel + ? _value.powerLevel + : powerLevel // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnRealmMemberImpl implements _SnRealmMember { + const _$SnRealmMemberImpl( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.realmId, + required this.accountId, + required this.realm, + required this.account, + required this.powerLevel}); + + factory _$SnRealmMemberImpl.fromJson(Map json) => + _$$SnRealmMemberImplFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final DateTime? deletedAt; + @override + final int realmId; + @override + final int accountId; + @override + final SnRealm realm; + @override + final SnAccount account; + @override + final int powerLevel; + + @override + String toString() { + return 'SnRealmMember(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, accountId: $accountId, realm: $realm, account: $account, powerLevel: $powerLevel)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnRealmMemberImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.realmId, realmId) || other.realmId == realmId) && + (identical(other.accountId, accountId) || + other.accountId == accountId) && + (identical(other.realm, realm) || other.realm == realm) && + (identical(other.account, account) || other.account == account) && + (identical(other.powerLevel, powerLevel) || + other.powerLevel == powerLevel)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, + deletedAt, realmId, accountId, realm, account, powerLevel); + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnRealmMemberImplCopyWith<_$SnRealmMemberImpl> get copyWith => + __$$SnRealmMemberImplCopyWithImpl<_$SnRealmMemberImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnRealmMemberImplToJson( + this, + ); + } +} + +abstract class _SnRealmMember implements SnRealmMember { + const factory _SnRealmMember( + {required final int id, + required final DateTime createdAt, + required final DateTime updatedAt, + required final DateTime? deletedAt, + required final int realmId, + required final int accountId, + required final SnRealm realm, + required final SnAccount account, + required final int powerLevel}) = _$SnRealmMemberImpl; + + factory _SnRealmMember.fromJson(Map json) = + _$SnRealmMemberImpl.fromJson; + + @override + int get id; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + @override + DateTime? get deletedAt; + @override + int get realmId; + @override + int get accountId; + @override + SnRealm get realm; + @override + SnAccount get account; + @override + int get powerLevel; + + /// Create a copy of SnRealmMember + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnRealmMemberImplCopyWith<_$SnRealmMemberImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SnRealm _$SnRealmFromJson(Map json) { + return _SnRealm.fromJson(json); +} + +/// @nodoc +mixin _$SnRealm { + int get id => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + DateTime? get deletedAt => throw _privateConstructorUsedError; + String get alias => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + List? get members => throw _privateConstructorUsedError; + String? get avatar => throw _privateConstructorUsedError; + String? get banner => throw _privateConstructorUsedError; + Map? get accessPolicy => throw _privateConstructorUsedError; + bool get isPublic => throw _privateConstructorUsedError; + bool get isCommunity => throw _privateConstructorUsedError; + int get accountId => throw _privateConstructorUsedError; + + /// Serializes this SnRealm to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnRealm + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnRealmCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnRealmCopyWith<$Res> { + factory $SnRealmCopyWith(SnRealm value, $Res Function(SnRealm) then) = + _$SnRealmCopyWithImpl<$Res, SnRealm>; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String alias, + String name, + String description, + List? members, + String? avatar, + String? banner, + Map? accessPolicy, + bool isPublic, + bool isCommunity, + int accountId}); +} + +/// @nodoc +class _$SnRealmCopyWithImpl<$Res, $Val extends SnRealm> + implements $SnRealmCopyWith<$Res> { + _$SnRealmCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnRealm + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? alias = null, + Object? name = null, + Object? description = null, + Object? members = freezed, + Object? avatar = freezed, + Object? banner = freezed, + Object? accessPolicy = freezed, + Object? isPublic = null, + Object? isCommunity = null, + Object? accountId = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + alias: null == alias + ? _value.alias + : alias // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + members: freezed == members + ? _value.members + : members // ignore: cast_nullable_to_non_nullable + as List?, + avatar: freezed == avatar + ? _value.avatar + : avatar // ignore: cast_nullable_to_non_nullable + as String?, + banner: freezed == banner + ? _value.banner + : banner // ignore: cast_nullable_to_non_nullable + as String?, + accessPolicy: freezed == accessPolicy + ? _value.accessPolicy + : accessPolicy // ignore: cast_nullable_to_non_nullable + as Map?, + isPublic: null == isPublic + ? _value.isPublic + : isPublic // ignore: cast_nullable_to_non_nullable + as bool, + isCommunity: null == isCommunity + ? _value.isCommunity + : isCommunity // ignore: cast_nullable_to_non_nullable + as bool, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnRealmImplCopyWith<$Res> implements $SnRealmCopyWith<$Res> { + factory _$$SnRealmImplCopyWith( + _$SnRealmImpl value, $Res Function(_$SnRealmImpl) then) = + __$$SnRealmImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + DateTime? deletedAt, + String alias, + String name, + String description, + List? members, + String? avatar, + String? banner, + Map? accessPolicy, + bool isPublic, + bool isCommunity, + int accountId}); +} + +/// @nodoc +class __$$SnRealmImplCopyWithImpl<$Res> + extends _$SnRealmCopyWithImpl<$Res, _$SnRealmImpl> + implements _$$SnRealmImplCopyWith<$Res> { + __$$SnRealmImplCopyWithImpl( + _$SnRealmImpl _value, $Res Function(_$SnRealmImpl) _then) + : super(_value, _then); + + /// Create a copy of SnRealm + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? alias = null, + Object? name = null, + Object? description = null, + Object? members = freezed, + Object? avatar = freezed, + Object? banner = freezed, + Object? accessPolicy = freezed, + Object? isPublic = null, + Object? isCommunity = null, + Object? accountId = null, + }) { + return _then(_$SnRealmImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + alias: null == alias + ? _value.alias + : alias // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + members: freezed == members + ? _value._members + : members // ignore: cast_nullable_to_non_nullable + as List?, + avatar: freezed == avatar + ? _value.avatar + : avatar // ignore: cast_nullable_to_non_nullable + as String?, + banner: freezed == banner + ? _value.banner + : banner // ignore: cast_nullable_to_non_nullable + as String?, + accessPolicy: freezed == accessPolicy + ? _value._accessPolicy + : accessPolicy // ignore: cast_nullable_to_non_nullable + as Map?, + isPublic: null == isPublic + ? _value.isPublic + : isPublic // ignore: cast_nullable_to_non_nullable + as bool, + isCommunity: null == isCommunity + ? _value.isCommunity + : isCommunity // ignore: cast_nullable_to_non_nullable + as bool, + accountId: null == accountId + ? _value.accountId + : accountId // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnRealmImpl implements _SnRealm { + const _$SnRealmImpl( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.alias, + required this.name, + required this.description, + required final List? members, + required this.avatar, + required this.banner, + required final Map? accessPolicy, + required this.isPublic, + required this.isCommunity, + required this.accountId}) + : _members = members, + _accessPolicy = accessPolicy; + + factory _$SnRealmImpl.fromJson(Map json) => + _$$SnRealmImplFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final DateTime? deletedAt; + @override + final String alias; + @override + final String name; + @override + final String description; + final List? _members; + @override + List? get members { + final value = _members; + if (value == null) return null; + if (_members is EqualUnmodifiableListView) return _members; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + final String? avatar; + @override + final String? banner; + final Map? _accessPolicy; + @override + Map? get accessPolicy { + final value = _accessPolicy; + if (value == null) return null; + if (_accessPolicy is EqualUnmodifiableMapView) return _accessPolicy; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final bool isPublic; + @override + final bool isCommunity; + @override + final int accountId; + + @override + String toString() { + return 'SnRealm(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, members: $members, avatar: $avatar, banner: $banner, accessPolicy: $accessPolicy, isPublic: $isPublic, isCommunity: $isCommunity, accountId: $accountId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnRealmImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.deletedAt, deletedAt) || + other.deletedAt == deletedAt) && + (identical(other.alias, alias) || other.alias == alias) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + const DeepCollectionEquality().equals(other._members, _members) && + (identical(other.avatar, avatar) || other.avatar == avatar) && + (identical(other.banner, banner) || other.banner == banner) && + const DeepCollectionEquality() + .equals(other._accessPolicy, _accessPolicy) && + (identical(other.isPublic, isPublic) || + other.isPublic == isPublic) && + (identical(other.isCommunity, isCommunity) || + other.isCommunity == isCommunity) && + (identical(other.accountId, accountId) || + other.accountId == accountId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + deletedAt, + alias, + name, + description, + const DeepCollectionEquality().hash(_members), + avatar, + banner, + const DeepCollectionEquality().hash(_accessPolicy), + isPublic, + isCommunity, + accountId); + + /// Create a copy of SnRealm + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnRealmImplCopyWith<_$SnRealmImpl> get copyWith => + __$$SnRealmImplCopyWithImpl<_$SnRealmImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnRealmImplToJson( + this, + ); + } +} + +abstract class _SnRealm implements SnRealm { + const factory _SnRealm( + {required final int id, + required final DateTime createdAt, + required final DateTime updatedAt, + required final DateTime? deletedAt, + required final String alias, + required final String name, + required final String description, + required final List? members, + required final String? avatar, + required final String? banner, + required final Map? accessPolicy, + required final bool isPublic, + required final bool isCommunity, + required final int accountId}) = _$SnRealmImpl; + + factory _SnRealm.fromJson(Map json) = _$SnRealmImpl.fromJson; + + @override + int get id; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + @override + DateTime? get deletedAt; + @override + String get alias; + @override + String get name; + @override + String get description; + @override + List? get members; + @override + String? get avatar; + @override + String? get banner; + @override + Map? get accessPolicy; + @override + bool get isPublic; + @override + bool get isCommunity; + @override + int get accountId; + + /// Create a copy of SnRealm + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnRealmImplCopyWith<_$SnRealmImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/types/realm.g.dart b/lib/types/realm.g.dart new file mode 100644 index 0000000..88e67bd --- /dev/null +++ b/lib/types/realm.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'realm.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SnRealmMemberImpl _$$SnRealmMemberImplFromJson(Map json) => + _$SnRealmMemberImpl( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + realmId: (json['realm_id'] as num).toInt(), + accountId: (json['account_id'] as num).toInt(), + realm: SnRealm.fromJson(json['realm'] as Map), + account: SnAccount.fromJson(json['account'] as Map), + powerLevel: (json['power_level'] as num).toInt(), + ); + +Map _$$SnRealmMemberImplToJson(_$SnRealmMemberImpl instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'realm_id': instance.realmId, + 'account_id': instance.accountId, + 'realm': instance.realm.toJson(), + 'account': instance.account.toJson(), + 'power_level': instance.powerLevel, + }; + +_$SnRealmImpl _$$SnRealmImplFromJson(Map json) => + _$SnRealmImpl( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + alias: json['alias'] as String, + name: json['name'] as String, + description: json['description'] as String, + members: (json['members'] as List?) + ?.map((e) => SnRealmMember.fromJson(e as Map)) + .toList(), + avatar: json['avatar'] as String?, + banner: json['banner'] as String?, + accessPolicy: json['access_policy'] as Map?, + isPublic: json['is_public'] as bool, + isCommunity: json['is_community'] as bool, + accountId: (json['account_id'] as num).toInt(), + ); + +Map _$$SnRealmImplToJson(_$SnRealmImpl instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'alias': instance.alias, + 'name': instance.name, + 'description': instance.description, + 'members': instance.members?.map((e) => e.toJson()).toList(), + 'avatar': instance.avatar, + 'banner': instance.banner, + 'access_policy': instance.accessPolicy, + 'is_public': instance.isPublic, + 'is_community': instance.isCommunity, + 'account_id': instance.accountId, + }; diff --git a/pubspec.lock b/pubspec.lock index 6a5a7b5..a1d691a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -658,6 +658,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 473a920..af5c5ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,8 @@ dependencies: collection: ^1.18.0 mime: ^2.0.0 web_socket_channel: ^3.0.1 + hive: ^2.2.3 + hive_flutter: ^1.1.0 dev_dependencies: flutter_test: @@ -90,6 +92,7 @@ dev_dependencies: json_serializable: ^6.8.0 icons_launcher: ^3.0.0 flutter_native_splash: ^2.4.2 + hive_generator: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec