diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index 918b923..9c84167 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -2,6 +2,7 @@ "appName": "Solar Network", "explore": "Explore", "chat": "Chat", + "realm": "Realm", "account": "Account", "riskDetection": "Risk Detection", "signIn": "Sign In", @@ -74,6 +75,18 @@ "postEditNotify": "You are about editing a post that already published.", "reactionAdded": "Your reaction has been added.", "reactionRemoved": "Your reaction has been removed.", + "realmNew": "New Realm", + "realmNewCreate": "Create a realm", + "realmNewJoin": "Join a exists realm", + "realmUsage": "Realm", + "realmUsageCaption": "Realm is a place to organize your posts, channels and more. It will be much easier if you build community with realm!", + "realmEstablish": "Establish a realm", + "realmEditNotify": "You are about editing a existing realm.", + "realmAliasLabel": "Realm Alias", + "realmNameLabel": "Realm Name", + "realmDescriptionLabel": "Realm Description", + "realmPublicLabel": "It's public", + "realmCommunityLabel": "It's community realm", "chatNew": "New Chat", "chatNewCreate": "Create a channel", "chatNewJoin": "Join a exists channel", diff --git a/lib/main.dart b/lib/main.dart index ece1790..6ca81d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:solian/providers/chat.dart'; import 'package:solian/providers/friend.dart'; import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/notify.dart'; +import 'package:solian/providers/realm.dart'; import 'package:solian/router.dart'; import 'package:solian/utils/theme.dart'; import 'package:solian/utils/timeago.dart'; @@ -43,6 +44,7 @@ class SolianApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => ChatProvider()), ChangeNotifierProvider(create: (_) => NotifyProvider()), ChangeNotifierProvider(create: (_) => FriendProvider()), + ChangeNotifierProvider(create: (_) => RealmProvider()), ], child: Overlay( initialEntries: [ diff --git a/lib/models/notification.dart b/lib/models/notification.dart index 189f8f1..751442e 100755 --- a/lib/models/notification.dart +++ b/lib/models/notification.dart @@ -9,7 +9,7 @@ class Notification { bool isImportant; bool isRealtime; DateTime? readAt; - int senderId; + int? senderId; int recipientId; Notification({ @@ -23,7 +23,7 @@ class Notification { required this.isImportant, required this.isRealtime, this.readAt, - required this.senderId, + this.senderId, required this.recipientId, }); @@ -34,9 +34,7 @@ class Notification { deletedAt: json['deleted_at'], subject: json['subject'], content: json['content'], - links: json['links'] != null - ? List.from(json['links'].map((x) => Link.fromJson(x))) - : List.empty(), + links: json['links'] != null ? List.from(json['links'].map((x) => Link.fromJson(x))) : List.empty(), isImportant: json['is_important'], isRealtime: json['is_realtime'], readAt: json['read_at'], @@ -51,9 +49,7 @@ class Notification { 'deleted_at': deletedAt, 'subject': subject, 'content': content, - 'links': links != null - ? List.from(links!.map((x) => x.toJson())) - : List.empty(), + 'links': links != null ? List.from(links!.map((x) => x.toJson())) : List.empty(), 'is_important': isImportant, 'is_realtime': isRealtime, 'read_at': readAt, diff --git a/lib/models/realm.dart b/lib/models/realm.dart new file mode 100644 index 0000000..f451a98 --- /dev/null +++ b/lib/models/realm.dart @@ -0,0 +1,55 @@ +class Realm { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String alias; + String name; + String description; + dynamic members; + bool isPublic; + bool isCommunity; + int accountId; + + Realm({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.alias, + required this.name, + required this.description, + required this.members, + required this.isPublic, + required this.isCommunity, + required this.accountId, + }); + + factory Realm.fromJson(Map json) => Realm( + id: json['id'], + createdAt: DateTime.parse(json['created_at']), + updatedAt: DateTime.parse(json['updated_at']), + deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null, + alias: json['alias'], + name: json['name'], + description: json['description'], + members: json['members'], + isPublic: json['is_public'], + isCommunity: json['is_community'], + accountId: json['account_id'], + ); + + Map toJson() => { + 'id': id, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'deleted_at': deletedAt, + 'alias': alias, + 'name': name, + 'description': description, + 'members': members, + 'is_public': isPublic, + 'is_community': isCommunity, + 'account_id': accountId, + }; +} \ No newline at end of file diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index bb50e43..893c25c 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -44,10 +44,10 @@ class ChatProvider extends ChangeNotifier { return channel; } - Future fetchChannel(String alias) async { + Future fetchChannel(String alias, String realm) async { final Client client = Client(); - var uri = getRequestUri('messaging', '/api/channels/global/$alias'); + var uri = getRequestUri('messaging', '/api/channels/$realm/$alias'); var res = await client.get(uri); if (res.statusCode == 200) { final result = jsonDecode(utf8.decode(res.bodyBytes)); @@ -60,10 +60,10 @@ class ChatProvider extends ChangeNotifier { } } - Future fetchOngoingCall(String alias) async { + Future fetchOngoingCall(String alias, String realm) async { final Client client = Client(); - var uri = getRequestUri('messaging', '/api/channels/global/$alias/calls/ongoing'); + var uri = getRequestUri('messaging', '/api/channels/$realm/$alias/calls/ongoing'); var res = await client.get(uri); if (res.statusCode == 200) { final result = jsonDecode(utf8.decode(res.bodyBytes)); diff --git a/lib/providers/realm.dart b/lib/providers/realm.dart new file mode 100644 index 0000000..cb62c7a --- /dev/null +++ b/lib/providers/realm.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:solian/models/realm.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/utils/service_url.dart'; + +class RealmProvider with ChangeNotifier { + List realms = List.empty(); + + Realm? focusRealm; + + Future fetch(AuthProvider auth) async { + if (!await auth.isAuthorized()) return; + + var uri = getRequestUri('passport', '/api/realms/me/available'); + + var res = await auth.client!.get(uri); + if (res.statusCode == 200) { + final result = jsonDecode(utf8.decode(res.bodyBytes)) as List; + realms = result.map((x) => Realm.fromJson(x)).toList(); + notifyListeners(); + } else { + var message = utf8.decode(res.bodyBytes); + throw Exception(message); + } + } + + Future fetchSingle(AuthProvider auth, String alias) async { + var uri = getRequestUri('passport', '/api/realms/$alias'); + var res = await auth.client!.get(uri); + if (res.statusCode == 200) { + final result = jsonDecode(utf8.decode(res.bodyBytes)); + focusRealm = Realm.fromJson(result); + notifyListeners(); + return focusRealm!; + } else { + var message = utf8.decode(res.bodyBytes); + throw Exception(message); + } + } +} diff --git a/lib/router.dart b/lib/router.dart index deefc7f..0f6e0e3 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:solian/models/call.dart'; import 'package:solian/models/channel.dart'; import 'package:solian/models/post.dart'; +import 'package:solian/models/realm.dart'; import 'package:solian/screens/account.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; @@ -19,6 +20,9 @@ import 'package:solian/screens/posts/comment_editor.dart'; import 'package:solian/screens/posts/moment_editor.dart'; import 'package:solian/screens/posts/screen.dart'; import 'package:solian/screens/auth/signin.dart'; +import 'package:solian/screens/realms/realm.dart'; +import 'package:solian/screens/realms/realm_editor.dart'; +import 'package:solian/screens/realms/realm_list.dart'; import 'package:solian/screens/users/userinfo.dart'; import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/empty.dart'; @@ -65,7 +69,7 @@ abstract class SolianRouter { ), GoRoute( path: '/posts/:dataset/:alias', - name: 'posts.screen', + name: 'posts.details', builder: (context, state) => PostScreen( alias: state.pathParameters['alias'] as String, dataset: state.pathParameters['dataset'] as String, @@ -73,6 +77,73 @@ abstract class SolianRouter { ), ], ), + ShellRoute( + pageBuilder: (context, state, child) => defaultPageBuilder( + context, + state, + SolianTheme.isLargeScreen(context) + ? TwoColumnLayout( + sideChild: const RealmListScreen(), + mainChild: child, + ) + : child, + ), + routes: [ + GoRoute( + path: '/realms', + name: 'realms', + builder: (context, state) => + !SolianTheme.isLargeScreen(context) ? const RealmListScreen() : const PageEmptyWidget(), + ), + GoRoute( + path: '/realms/editor', + name: 'realms.editor', + builder: (context, state) => RealmEditorScreen( + editing: state.extra as Realm?, + realm: state.uri.queryParameters['realm'], + ), + ), + GoRoute( + path: '/realms/:realm/posts/:dataset/:alias', + name: 'realms.posts.details', + builder: (context, state) => PostScreen( + alias: state.pathParameters['alias'] as String, + dataset: state.pathParameters['dataset'] as String, + ), + ), + GoRoute( + path: '/realms/:realm', + name: 'realms.details', + builder: (context, state) => !SolianTheme.isLargeScreen(context) + ? RealmScreen(alias: state.pathParameters['realm'] as String) + : const PageEmptyWidget(), + ), + GoRoute( + path: '/realms/:realm/chat/:channel', + name: 'realms.chat.channel', + builder: (context, state) => ChatScreen( + alias: state.pathParameters['channel'] as String, + realm: state.pathParameters['realm'] as String, + ), + ), + GoRoute( + path: '/realms/:realm/chat/:channel/manage', + name: 'realms.chat.channel.manage', + builder: (context, state) => ChatDetailScreen( + channel: state.extra as Channel, + realm: state.pathParameters['realm'] as String, + ), + ), + GoRoute( + path: '/realms/:realm/chat/:channel/member', + name: 'realms.chat.channel.member', + builder: (context, state) => ChatMemberScreen( + channel: state.extra as Channel, + realm: state.pathParameters['realm'] as String, + ), + ), + ], + ), ShellRoute( pageBuilder: (context, state, child) => defaultPageBuilder( context, @@ -92,32 +163,35 @@ abstract class SolianRouter { !SolianTheme.isLargeScreen(context) ? const ChatListScreen() : const PageEmptyWidget(), ), GoRoute( - path: '/chat/create', + path: '/chat/editor', name: 'chat.channel.editor', - builder: (context, state) => ChannelEditorScreen(editing: state.extra as Channel?), + builder: (context, state) => ChannelEditorScreen( + editing: state.extra as Channel?, + realm: state.uri.queryParameters['realm'], + ), ), GoRoute( - path: '/chat/c/:channel', + path: '/chat/:channel', name: 'chat.channel', builder: (context, state) => ChatScreen(alias: state.pathParameters['channel'] as String), ), GoRoute( - path: '/chat/c/:channel/call', - name: 'chat.channel.call', - builder: (context, state) => ChatCall(call: state.extra as Call), - ), - GoRoute( - path: '/chat/c/:channel/manage', + path: '/chat/:channel/manage', name: 'chat.channel.manage', builder: (context, state) => ChatDetailScreen(channel: state.extra as Channel), ), GoRoute( - path: '/chat/c/:channel/member', + path: '/chat/:channel/member', name: 'chat.channel.member', builder: (context, state) => ChatMemberScreen(channel: state.extra as Channel), ), ], ), + GoRoute( + path: '/chat/:channel/call', + name: 'chat.channel.call', + builder: (context, state) => ChatCall(call: state.extra as Call), + ), ShellRoute( pageBuilder: (context, state, child) => defaultPageBuilder( context, diff --git a/lib/screens/chat/channel/channel_editor.dart b/lib/screens/chat/channel/channel_editor.dart index 9675196..0bb71b7 100644 --- a/lib/screens/chat/channel/channel_editor.dart +++ b/lib/screens/chat/channel/channel_editor.dart @@ -15,8 +15,9 @@ import 'package:uuid/uuid.dart'; class ChannelEditorScreen extends StatefulWidget { final Channel? editing; + final String? realm; - const ChannelEditorScreen({super.key, this.editing}); + const ChannelEditorScreen({super.key, this.editing, this.realm}); @override State createState() => _ChannelEditorScreenState(); @@ -39,8 +40,8 @@ class _ChannelEditorScreenState extends State { } final uri = widget.editing == null - ? getRequestUri('messaging', '/api/channels/global') - : getRequestUri('messaging', '/api/channels/global/${widget.editing!.id}'); + ? getRequestUri('messaging', '/api/channels/${widget.realm ?? 'global'}') + : getRequestUri('messaging', '/api/channels/${widget.realm ?? 'global'}/${widget.editing!.id}'); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); req.headers['Content-Type'] = 'application/json'; diff --git a/lib/screens/chat/channel/channel_member.dart b/lib/screens/chat/channel/channel_member.dart index 210b491..e71b2e9 100644 --- a/lib/screens/chat/channel/channel_member.dart +++ b/lib/screens/chat/channel/channel_member.dart @@ -15,8 +15,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ChatMemberScreen extends StatefulWidget { final Channel channel; + final String realm; - const ChatMemberScreen({super.key, required this.channel}); + const ChatMemberScreen({super.key, required this.channel, this.realm = 'global'}); @override State createState() => _ChatMemberScreenState(); @@ -36,8 +37,7 @@ class _ChatMemberScreenState extends State { _selfId = prof['id']; - var uri = getRequestUri( - 'messaging', '/api/channels/global/${widget.channel.alias}/members'); + var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/members'); var res = await auth.client!.get(uri); if (res.statusCode == 200) { @@ -51,7 +51,7 @@ class _ChatMemberScreenState extends State { } } - Future kickMember(ChannelMember item) async { + Future removeMember(ChannelMember item) async { setState(() => _isSubmitting = true); final auth = context.read(); @@ -60,10 +60,9 @@ class _ChatMemberScreenState extends State { return; } - var uri = getRequestUri( - 'messaging', '/api/channels/global/${widget.channel.alias}/kick'); + var uri = getRequestUri('messaging', '/api/channels/global/${widget.channel.alias}'); - var res = await auth.client!.post( + var res = await auth.client!.delete( uri, headers: { 'Content-Type': 'application/json', @@ -82,7 +81,7 @@ class _ChatMemberScreenState extends State { setState(() => _isSubmitting = false); } - Future inviteMember(String username) async { + Future addMember(String username) async { setState(() => _isSubmitting = true); final auth = context.read(); @@ -91,8 +90,7 @@ class _ChatMemberScreenState extends State { return; } - var uri = getRequestUri( - 'messaging', '/api/channels/global/${widget.channel.alias}/invite'); + var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}'); var res = await auth.client!.post( uri, @@ -122,10 +120,10 @@ class _ChatMemberScreenState extends State { ); if (input == null) return; - await inviteMember((input as Account).name); + await addMember((input as Account).name); } - bool getKickable(ChannelMember item) { + bool getRemovable(ChannelMember item) { if (_selfId != widget.channel.account.externalId) return false; if (item.accountId == widget.channel.accountId) return false; if (item.account.externalId == _selfId) return false; @@ -156,9 +154,7 @@ class _ChatMemberScreenState extends State { child: CustomScrollView( slivers: [ SliverToBoxAdapter( - child: _isSubmitting - ? const LinearProgressIndicator().animate().scaleX() - : Container(), + child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), ), SliverList.builder( itemCount: _members.length, @@ -169,9 +165,7 @@ class _ChatMemberScreenState extends State { return Dismissible( key: Key(randomId.toString()), - direction: getKickable(element) - ? DismissDirection.startToEnd - : DismissDirection.none, + direction: getRemovable(element) ? DismissDirection.startToEnd : DismissDirection.none, background: Container( color: Colors.red, padding: const EdgeInsets.symmetric(horizontal: 20), @@ -179,13 +173,12 @@ class _ChatMemberScreenState extends State { child: const Icon(Icons.remove, color: Colors.white), ), child: ListTile( - leading: AccountAvatar( - source: element.account.avatar, direct: true), + leading: AccountAvatar(source: element.account.avatar, direct: true), title: Text(element.account.nick), subtitle: Text(element.account.name), ), onDismissed: (_) { - kickMember(element); + removeMember(element); }, ); }, diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index d2fbc5d..3f93b71 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -21,8 +21,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ChatScreen extends StatelessWidget { final String alias; + final String realm; - const ChatScreen({super.key, required this.alias}); + const ChatScreen({super.key, required this.alias, this.realm = 'global'}); @override Widget build(BuildContext context) { @@ -37,31 +38,35 @@ class ChatScreen extends StatelessWidget { ChannelCallAction( call: chat.ongoingCall, channel: chat.focusChannel!, - onUpdate: () => chat.fetchChannel(chat.focusChannel!.alias), + realm: realm, + onUpdate: () => chat.fetchChannel(chat.focusChannel!.alias, realm), ), ChannelManageAction( channel: chat.focusChannel!, - onUpdate: () => chat.fetchChannel(chat.focusChannel!.alias), + realm: realm, + onUpdate: () => chat.fetchChannel(chat.focusChannel!.alias, realm), ), ] : [], - child: ChatScreenWidget( + child: ChatWidget( alias: alias, + realm: realm, ), ); } } -class ChatScreenWidget extends StatefulWidget { +class ChatWidget extends StatefulWidget { final String alias; + final String realm; - const ChatScreenWidget({super.key, required this.alias}); + const ChatWidget({super.key, required this.alias, required this.realm}); @override - State createState() => _ChatScreenWidgetState(); + State createState() => _ChatWidgetState(); } -class _ChatScreenWidgetState extends State { +class _ChatWidgetState extends State { bool _isReady = false; final PagingController _pagingController = PagingController(firstPageKey: 0); @@ -77,7 +82,7 @@ class _ChatScreenWidgetState extends State { var uri = getRequestUri( 'messaging', - '/api/channels/global/${widget.alias}/messages?take=$take&offset=$offset', + '/api/channels/${widget.realm}/${widget.alias}/messages?take=$take&offset=$offset', ); var res = await auth.client!.get(uri); @@ -147,8 +152,8 @@ class _ChatScreenWidgetState extends State { super.initState(); Future.delayed(Duration.zero, () { - _chat.fetchOngoingCall(widget.alias); - _chat.fetchChannel(widget.alias); + _chat.fetchOngoingCall(widget.alias, widget.realm); + _chat.fetchChannel(widget.alias, widget.realm); }); } @@ -232,6 +237,7 @@ class _ChatScreenWidgetState extends State { ), ), ChatMessageEditor( + realm: widget.realm, channel: widget.alias, editing: _editingItem, replying: _replyingItem, diff --git a/lib/screens/chat/chat_detail.dart b/lib/screens/chat/chat_detail.dart index 42baf8b..d24f27d 100644 --- a/lib/screens/chat/chat_detail.dart +++ b/lib/screens/chat/chat_detail.dart @@ -9,8 +9,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ChatDetailScreen extends StatefulWidget { final Channel channel; + final String realm; - const ChatDetailScreen({super.key, required this.channel}); + const ChatDetailScreen({super.key, required this.channel, this.realm = 'global'}); @override State createState() => _ChatDetailScreenState(); @@ -24,6 +25,7 @@ class _ChatDetailScreenState extends State { context: context, builder: (context) => ChannelDeletion( channel: widget.channel, + realm: widget.realm, isOwned: _isOwned, ), ); diff --git a/lib/screens/chat/chat_list.dart b/lib/screens/chat/chat_list.dart index 7aa686d..8ce03c6 100644 --- a/lib/screens/chat/chat_list.dart +++ b/lib/screens/chat/chat_list.dart @@ -23,29 +23,15 @@ class ChatListScreen extends StatelessWidget { title: AppLocalizations.of(context)!.chat, appBarActions: const [NotificationButton()], fixedAppBarColor: SolianTheme.isLargeScreen(context), - child: ChatListWidget( - onSelect: (item) { - if (SolianRouter.currentRoute.name == 'chat.channel') { - SolianRouter.router.pushReplacementNamed( - 'chat.channel', - pathParameters: {'channel': item.alias}, - ); - } else { - SolianRouter.router.pushNamed( - 'chat.channel', - pathParameters: {'channel': item.alias}, - ); - } - }, - ), + child: const ChatListWidget(), ); } } class ChatListWidget extends StatefulWidget { - final Function(Channel item)? onSelect; + final String? realm; - const ChatListWidget({super.key, this.onSelect}); + const ChatListWidget({super.key, this.realm}); @override State createState() => _ChatListWidgetState(); @@ -58,7 +44,12 @@ class _ChatListWidgetState extends State { final auth = context.read(); if (!await auth.isAuthorized()) return; - var uri = getRequestUri('messaging', '/api/channels/global/me/available'); + Uri uri; + if (widget.realm == null) { + uri = getRequestUri('messaging', '/api/channels/global/me/available'); + } else { + uri = getRequestUri('messaging', '/api/channels/${widget.realm}'); + } var res = await auth.client!.get(uri); if (res.statusCode == 200) { @@ -75,7 +66,10 @@ class _ChatListWidgetState extends State { void viewNewChatAction() { showModalBottomSheet( context: context, - builder: (context) => ChatNewAction(onUpdate: () => fetchChannels()), + builder: (context) => ChatNewAction( + onUpdate: () => fetchChannels(), + realm: widget.realm, + ), ); } @@ -128,17 +122,24 @@ class _ChatListWidgetState extends State { title: Text(element.name), subtitle: Text(element.description), onTap: () async { - if (widget.onSelect != null) { - widget.onSelect!(element); - return; + String? result; + if (SolianRouter.currentRoute.name == 'chat.channel') { + result = await SolianRouter.router.pushReplacementNamed( + widget.realm == null ? 'chat.channel' : 'realms.chat.channel', + pathParameters: { + 'channel': element.alias, + ...(widget.realm == null ? {} : {'realm': widget.realm!}), + }, + ); + } else { + result = await SolianRouter.router.pushNamed( + widget.realm == null ? 'chat.channel' : 'realms.chat.channel', + pathParameters: { + 'channel': element.alias, + ...(widget.realm == null ? {} : {'realm': widget.realm!}), + }, + ); } - - final result = await SolianRouter.router.pushNamed( - 'chat.channel', - pathParameters: { - 'channel': element.alias, - }, - ); switch (result) { case 'refresh': fetchChannels(); diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 19a7bb9..1f55582 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -25,25 +25,15 @@ class ExplorePostScreen extends StatelessWidget { fixedAppBarColor: SolianTheme.isLargeScreen(context), appBarActions: const [NotificationButton()], title: AppLocalizations.of(context)!.explore, - child: ExplorePostWidget( - onSelect: (item) { - SolianRouter.router.pushNamed( - 'posts.screen', - pathParameters: { - 'alias': item.alias, - 'dataset': item.dataset, - }, - ); - }, - ), + child: const ExplorePostWidget(), ); } } class ExplorePostWidget extends StatefulWidget { - final Function(Post item) onSelect; + final String? realm; - const ExplorePostWidget({super.key, required this.onSelect}); + const ExplorePostWidget({super.key, this.realm}); @override State createState() => _ExplorePostWidgetState(); @@ -58,7 +48,12 @@ class _ExplorePostWidgetState extends State { final offset = pageKey; const take = 5; - var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset'); + Uri uri; + if (widget.realm == null) { + uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset'); + } else { + uri = getRequestUri('interactive', '/api/feed?realm=${widget.realm}&take=$take&offset=$offset'); + } var res = await _client.get(uri); if (res.statusCode == 200) { @@ -95,7 +90,10 @@ class _ExplorePostWidgetState extends State { return FloatingActionButton( child: const Icon(Icons.edit), onPressed: () async { - final did = await SolianRouter.router.pushNamed('posts.moments.editor'); + final did = await SolianRouter.router.pushNamed( + 'posts.moments.editor', + queryParameters: {'realm': widget.realm}, + ); if (did == true) _pagingController.refresh(); }, ); @@ -114,7 +112,16 @@ class _ExplorePostWidgetState extends State { itemBuilder: (context, item, index) => PostItem( item: item, onUpdate: () => _pagingController.refresh(), - onTap: () => widget.onSelect(item), + onTap: () { + SolianRouter.router.pushNamed( + widget.realm == null ? 'posts.details' : 'realms.posts.details', + pathParameters: { + 'alias': item.alias, + 'dataset': item.dataset, + ...(widget.realm == null ? {} : {'realm': widget.realm!}), + }, + ); + }, ), ), ), diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index 43a113c..60a9cc4 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -37,12 +37,16 @@ class _NotificationScreenState extends State { markList.add(element.id); } - var uri = getRequestUri('passport', '/api/notifications/batch/read'); - await auth.client!.put( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'messages': markList}), - ); + nty.clearRealtime(); + + if(markList.isNotEmpty) { + var uri = getRequestUri('passport', '/api/notifications/batch/read'); + await auth.client!.put( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'messages': markList}), + ); + } ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.notifyMarkAllReadDone), @@ -195,19 +199,7 @@ class NotificationItem extends StatelessWidget { onDismissed: (direction) { markAsRead(item, context).then((value) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: RichText( - text: TextSpan( - children: [ - TextSpan( - text: item.subject, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const TextSpan(text: ' is marked as read') - ], - ), - ), - ), + SnackBar(content: Text('${item.subject} is marked as read')), ); }); if (onDismiss != null) { diff --git a/lib/screens/realms/realm.dart b/lib/screens/realms/realm.dart new file mode 100644 index 0000000..5af5322 --- /dev/null +++ b/lib/screens/realms/realm.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/realm.dart'; +import 'package:solian/screens/chat/chat_list.dart'; +import 'package:solian/screens/explore.dart'; +import 'package:solian/utils/theme.dart'; +import 'package:solian/widgets/scaffold.dart'; + +class RealmScreen extends StatelessWidget { + final String alias; + + const RealmScreen({super.key, required this.alias}); + + @override + Widget build(BuildContext context) { + final realm = context.watch(); + + return IndentScaffold( + title: realm.focusRealm?.name ?? 'Loading...', + hideDrawer: !SolianTheme.isLargeScreen(context), + noSafeArea: true, + fixedAppBarColor: SolianTheme.isLargeScreen(context), + child: RealmWidget( + alias: alias, + ), + ); + } +} + +class RealmWidget extends StatefulWidget { + final String alias; + + const RealmWidget({super.key, required this.alias}); + + @override + State createState() => _RealmWidgetState(); +} + +class _RealmWidgetState extends State { + bool _isReady = false; + + late RealmProvider _realm; + + @override + void initState() { + super.initState(); + + Future.delayed(Duration.zero, () { + final auth = context.read(); + if (_realm.focusRealm?.alias != widget.alias) { + _realm.fetchSingle(auth, widget.alias); + } + }); + } + + @override + Widget build(BuildContext context) { + if (!_isReady) { + _realm = context.watch(); + _isReady = true; + } + + return DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + isScrollable: !SolianTheme.isLargeScreen(context), + tabs: const [ + Tab(icon: Icon(Icons.newspaper)), + Tab(icon: Icon(Icons.message)), + ], + ), + Expanded( + child: TabBarView( + children: [ + ExplorePostWidget(realm: widget.alias), + ChatListWidget(realm: widget.alias), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/screens/realms/realm_editor.dart b/lib/screens/realms/realm_editor.dart new file mode 100644 index 0000000..f97c0c3 --- /dev/null +++ b/lib/screens/realms/realm_editor.dart @@ -0,0 +1,208 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:http/http.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/models/realm.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/exts.dart'; +import 'package:solian/widgets/scaffold.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class RealmEditorScreen extends StatefulWidget { + final Realm? editing; + final String? realm; + + const RealmEditorScreen({super.key, this.editing, this.realm}); + + @override + State createState() => _RealmEditorScreenState(); +} + +class _RealmEditorScreenState extends State { + final _aliasController = TextEditingController(); + final _nameController = TextEditingController(); + final _descriptionController = TextEditingController(); + + bool _isPublic = false; + bool _isCommunity = false; + + bool _isSubmitting = false; + + Future applyChannel(BuildContext context) async { + setState(() => _isSubmitting = true); + + final auth = context.read(); + if (!await auth.isAuthorized()) { + setState(() => _isSubmitting = false); + return; + } + + final uri = widget.editing == null + ? getRequestUri('passport', '/api/realms') + : getRequestUri('passport', '/api/realms/${widget.editing!.id}'); + + final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); + req.headers['Content-Type'] = 'application/json'; + req.body = jsonEncode({ + 'alias': _aliasController.value.text.toLowerCase(), + 'name': _nameController.value.text, + 'description': _descriptionController.value.text, + 'is_public': _isPublic, + 'is_community': _isCommunity, + 'realm': widget.realm, + }); + + var res = await Response.fromStream(await auth.client!.send(req)); + if (res.statusCode != 200) { + var message = utf8.decode(res.bodyBytes); + context.showErrorDialog(message); + } else { + if (SolianRouter.router.canPop()) { + SolianRouter.router.pop(true); + } + } + setState(() => _isSubmitting = false); + } + + void randomizeAlias() { + _aliasController.text = const Uuid().v4().replaceAll('-', '').substring(0, 16); + } + + void cancelEditing() { + if (SolianRouter.router.canPop()) { + SolianRouter.router.pop(false); + } + } + + @override + void initState() { + if (widget.editing != null) { + _aliasController.text = widget.editing!.alias; + _nameController.text = widget.editing!.name; + _descriptionController.text = widget.editing!.description; + _isPublic = widget.editing!.isPublic; + _isCommunity = widget.editing!.isCommunity; + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final editingBanner = MaterialBanner( + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), + leading: const Icon(Icons.edit_note), + backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), + dividerColor: const Color.fromARGB(1, 0, 0, 0), + content: Text(AppLocalizations.of(context)!.realmEditNotify), + actions: [ + TextButton( + child: Text(AppLocalizations.of(context)!.cancel), + onPressed: () => cancelEditing(), + ), + ], + ); + + return IndentScaffold( + hideDrawer: true, + title: AppLocalizations.of(context)!.realmEstablish, + appBarActions: [ + TextButton( + onPressed: !_isSubmitting ? () => applyChannel(context) : null, + child: Text(AppLocalizations.of(context)!.apply.toUpperCase()), + ), + ], + child: Column( + children: [ + _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), + widget.editing != null ? editingBanner : Container(), + ListTile( + title: Text(AppLocalizations.of(context)!.realmUsage), + subtitle: Text(AppLocalizations.of(context)!.realmUsageCaption), + leading: const CircleAvatar( + backgroundColor: Colors.teal, + child: Icon(Icons.supervised_user_circle, color: Colors.white), + ), + ), + const Divider(thickness: 0.3), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + controller: _aliasController, + decoration: InputDecoration.collapsed( + hintText: AppLocalizations.of(context)!.realmAliasLabel, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + TextButton( + style: TextButton.styleFrom( + shape: const CircleBorder(), + visualDensity: const VisualDensity(horizontal: -2, vertical: -2), + ), + onPressed: () => randomizeAlias(), + child: const Icon(Icons.refresh), + ) + ], + ), + ), + const Divider(thickness: 0.3), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: TextField( + autocorrect: true, + controller: _nameController, + decoration: InputDecoration.collapsed( + hintText: AppLocalizations.of(context)!.realmNameLabel, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + const Divider(thickness: 0.3), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextField( + minLines: 5, + maxLines: null, + autocorrect: true, + keyboardType: TextInputType.multiline, + controller: _descriptionController, + decoration: InputDecoration.collapsed( + hintText: AppLocalizations.of(context)!.realmDescriptionLabel, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ), + const Divider(thickness: 0.3), + CheckboxListTile( + title: Text(AppLocalizations.of(context)!.realmPublicLabel), + value: _isPublic, + onChanged: (newValue) { + setState(() => _isPublic = newValue ?? false); + }, + controlAffinity: ListTileControlAffinity.leading, + ), + CheckboxListTile( + title: Text(AppLocalizations.of(context)!.realmCommunityLabel), + value: _isCommunity, + onChanged: (newValue) { + setState(() => _isCommunity = newValue ?? false); + }, + controlAffinity: ListTileControlAffinity.leading, + ) + ], + ), + ); + } +} diff --git a/lib/screens/realms/realm_list.dart b/lib/screens/realms/realm_list.dart new file mode 100644 index 0000000..dba0c53 --- /dev/null +++ b/lib/screens/realms/realm_list.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/realm.dart'; +import 'package:solian/router.dart'; +import 'package:solian/screens/realms/realm.dart'; +import 'package:solian/utils/theme.dart'; +import 'package:solian/widgets/notification_notifier.dart'; +import 'package:solian/widgets/realms/realm_new.dart'; +import 'package:solian/widgets/scaffold.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:solian/widgets/signin_required.dart'; + +class RealmListScreen extends StatelessWidget { + const RealmListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final realm = context.watch(); + + return realm.focusRealm == null + ? IndentScaffold( + title: AppLocalizations.of(context)!.realm, + appBarActions: const [NotificationButton()], + fixedAppBarColor: SolianTheme.isLargeScreen(context), + child: const RealmListWidget(), + ) + : RealmScreen(alias: realm.focusRealm!.alias); + } +} + +class RealmListWidget extends StatefulWidget { + const RealmListWidget({super.key}); + + @override + State createState() => _RealmListWidgetState(); +} + +class _RealmListWidgetState extends State { + void viewNewRealmAction() { + final auth = context.read(); + final realms = context.read(); + + showModalBottomSheet( + context: context, + builder: (context) => RealmNewAction(onUpdate: () => realms.fetch(auth)), + ); + } + + @override + void initState() { + Future.delayed(Duration.zero, () { + final auth = context.read(); + final realms = context.read(); + realms.fetch(auth); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final auth = context.read(); + final realms = context.watch(); + + return Scaffold( + floatingActionButton: FutureBuilder( + future: auth.isAuthorized(), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return FloatingActionButton.extended( + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.realmNew), + onPressed: () => viewNewRealmAction(), + ); + } else { + return Container(); + } + }, + ), + body: FutureBuilder( + future: auth.isAuthorized(), + builder: (context, snapshot) { + if (!snapshot.hasData || !snapshot.data!) { + return const SignInRequiredScreen(); + } + + return RefreshIndicator( + onRefresh: () => realms.fetch(auth), + child: ListView.builder( + itemCount: realms.realms.length, + itemBuilder: (context, index) { + final element = realms.realms[index]; + return ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.indigo, + child: Icon(Icons.supervisor_account, color: Colors.white), + ), + title: Text(element.name), + subtitle: Text(element.description), + onTap: () async { + realms.fetchSingle(auth, element.alias); + String? result; + if (SolianRouter.currentRoute.name == 'chat.channel') { + result = await SolianRouter.router.pushReplacementNamed( + 'realms.details', + pathParameters: { + 'realm': element.alias, + }, + ); + } else { + result = await SolianRouter.router.pushNamed( + 'realms.details', + pathParameters: { + 'realm': element.alias, + }, + ); + } + switch (result) { + case 'refresh': + realms.fetch(auth); + } + }, + ); + }, + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/chat/channel_action.dart b/lib/widgets/chat/channel_action.dart index 8c0537e..07a4513 100644 --- a/lib/widgets/chat/channel_action.dart +++ b/lib/widgets/chat/channel_action.dart @@ -13,9 +13,16 @@ import 'package:solian/widgets/exts.dart'; class ChannelCallAction extends StatefulWidget { final Call? call; final Channel channel; + final String realm; final Function onUpdate; - const ChannelCallAction({super.key, this.call, required this.channel, required this.onUpdate}); + const ChannelCallAction({ + super.key, + this.call, + required this.channel, + required this.onUpdate, + this.realm = 'global', + }); @override State createState() => _ChannelCallActionState(); @@ -33,7 +40,7 @@ class _ChannelCallActionState extends State { return; } - var uri = getRequestUri('messaging', '/api/channels/global/${widget.channel.alias}/calls'); + var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/calls'); var res = await auth.client!.post(uri); if (res.statusCode != 200) { @@ -54,7 +61,7 @@ class _ChannelCallActionState extends State { return; } - var uri = getRequestUri('messaging', '/api/channels/global/${widget.channel.alias}/calls/ongoing'); + var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/calls/ongoing'); var res = await auth.client!.delete(uri); if (res.statusCode != 200) { @@ -90,17 +97,26 @@ class _ChannelCallActionState extends State { class ChannelManageAction extends StatelessWidget { final Channel channel; final Function onUpdate; + final String realm; - const ChannelManageAction({super.key, required this.channel, required this.onUpdate}); + const ChannelManageAction({ + super.key, + required this.channel, + required this.onUpdate, + this.realm = 'global', + }); @override Widget build(BuildContext context) { return IconButton( onPressed: () async { final result = await SolianRouter.router.pushNamed( - 'chat.channel.manage', + realm == 'global' ? 'chat.channel.manage' : 'realms.chat.channel.manage', extra: channel, - pathParameters: {'channel': channel.alias}, + pathParameters: { + 'channel': channel.alias, + ...(realm == 'global' ? {} : {'realm': realm}), + }, ); switch (result) { case 'disposed': diff --git a/lib/widgets/chat/channel_deletion.dart b/lib/widgets/chat/channel_deletion.dart index 3ac4452..589bf75 100644 --- a/lib/widgets/chat/channel_deletion.dart +++ b/lib/widgets/chat/channel_deletion.dart @@ -10,10 +10,15 @@ import 'package:solian/widgets/exts.dart'; class ChannelDeletion extends StatefulWidget { final Channel channel; + final String realm; final bool isOwned; - const ChannelDeletion( - {super.key, required this.channel, required this.isOwned}); + const ChannelDeletion({ + super.key, + required this.channel, + required this.realm, + required this.isOwned, + }); @override State createState() => _ChannelDeletionState(); @@ -32,7 +37,7 @@ class _ChannelDeletionState extends State { } var res = await auth.client!.delete( - getRequestUri('messaging', '/api/channels/global/${widget.channel.id}'), + getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.id}'), ); if (res.statusCode != 200) { var message = utf8.decode(res.bodyBytes); @@ -53,8 +58,8 @@ class _ChannelDeletionState extends State { return; } - var res = await auth.client!.post( - getRequestUri('messaging', '/api/channels/global/${widget.channel.alias}/leave'), + var res = await auth.client!.delete( + getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/me'), ); if (res.statusCode != 200) { var message = utf8.decode(res.bodyBytes); diff --git a/lib/widgets/chat/chat_new.dart b/lib/widgets/chat/chat_new.dart index e1d4bb5..7e645b5 100644 --- a/lib/widgets/chat/chat_new.dart +++ b/lib/widgets/chat/chat_new.dart @@ -4,8 +4,9 @@ import 'package:solian/router.dart'; class ChatNewAction extends StatelessWidget { final Function onUpdate; + final String? realm; - const ChatNewAction({super.key, required this.onUpdate}); + const ChatNewAction({super.key, required this.onUpdate, this.realm}); @override Widget build(BuildContext context) { @@ -29,7 +30,10 @@ class ChatNewAction extends StatelessWidget { leading: const Icon(Icons.add), title: Text(AppLocalizations.of(context)!.chatNewCreate), onTap: () { - SolianRouter.router.pushNamed('chat.channel.editor').then((did) { + SolianRouter.router.pushNamed( + 'chat.channel.editor', + queryParameters: {'realm': realm}, + ).then((did) { if (did == true) { onUpdate(); if (Navigator.canPop(context)) { diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index 9b852fe..72c0eb0 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -14,11 +14,19 @@ import 'package:badges/badges.dart' as badge; class ChatMessageEditor extends StatefulWidget { final String channel; + final String realm; final Message? editing; final Message? replying; final Function? onReset; - const ChatMessageEditor({super.key, required this.channel, this.editing, this.replying, this.onReset}); + const ChatMessageEditor({ + super.key, + required this.channel, + this.realm = 'global', + this.editing, + this.replying, + this.onReset, + }); @override State createState() => _ChatMessageEditorState(); @@ -53,8 +61,8 @@ class _ChatMessageEditorState extends State { if (!await auth.isAuthorized()) return; final uri = widget.editing == null - ? getRequestUri('messaging', '/api/channels/global/${widget.channel}/messages') - : getRequestUri('messaging', '/api/channels/global/${widget.channel}/messages/${widget.editing!.id}'); + ? getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel}/messages') + : getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel}/messages/${widget.editing!.id}'); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri); req.headers['Content-Type'] = 'application/json'; diff --git a/lib/widgets/navigation_drawer.dart b/lib/widgets/navigation_drawer.dart index 1a856ac..071a327 100644 --- a/lib/widgets/navigation_drawer.dart +++ b/lib/widgets/navigation_drawer.dart @@ -42,6 +42,13 @@ class _SolianNavigationDrawerState extends State { ), 'explore', ), + ( + NavigationDrawerDestination( + icon: const Icon(Icons.supervised_user_circle), + label: Text(AppLocalizations.of(context)!.realm), + ), + 'realms', + ), ( NavigationDrawerDestination( icon: const Icon(Icons.send), diff --git a/lib/widgets/posts/attachment_screen.dart b/lib/widgets/posts/attachment_screen.dart index 8d84d3d..41fa434 100755 --- a/lib/widgets/posts/attachment_screen.dart +++ b/lib/widgets/posts/attachment_screen.dart @@ -1,4 +1,6 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:solian/utils/platform.dart'; class AttachmentScreen extends StatelessWidget { final String url; @@ -17,7 +19,7 @@ class AttachmentScreen extends StatelessWidget { maxScale: 16, panEnabled: true, scaleEnabled: true, - child: Image.network(url, fit: BoxFit.contain), + child: PlatformInfo.canCacheImage ? CachedNetworkImage(imageUrl: url, fit: BoxFit.contain) : Image.network(url), ), ); diff --git a/lib/widgets/posts/content/article.dart b/lib/widgets/posts/content/article.dart index c30d824..8d8ed31 100644 --- a/lib/widgets/posts/content/article.dart +++ b/lib/widgets/posts/content/article.dart @@ -1,9 +1,10 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:solian/models/post.dart'; import 'package:markdown/markdown.dart' as markdown; +import 'package:solian/utils/platform.dart'; import 'package:solian/utils/service_url.dart'; -import 'package:solian/widgets/posts/content/attachment.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ArticleContent extends StatelessWidget { @@ -61,10 +62,9 @@ class ArticleContent extends StatelessWidget { uri = url; } - return AttachmentItem( - type: 1, - url: uri.toString(), - ); + return PlatformInfo.canCacheImage + ? CachedNetworkImage(imageUrl: uri.toString()) + : Image.network(uri.toString()); }, ), ], diff --git a/lib/widgets/posts/post.dart b/lib/widgets/posts/post.dart index 9500ea6..c9fe73f 100644 --- a/lib/widgets/posts/post.dart +++ b/lib/widgets/posts/post.dart @@ -1,6 +1,4 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/models/post.dart'; import 'package:solian/router.dart'; diff --git a/lib/widgets/realms/realm_new.dart b/lib/widgets/realms/realm_new.dart new file mode 100644 index 0000000..2b2b085 --- /dev/null +++ b/lib/widgets/realms/realm_new.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:solian/router.dart'; + +class RealmNewAction extends StatelessWidget { + final Function onUpdate; + + const RealmNewAction({super.key, required this.onUpdate}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 320, + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(left: 20, top: 20, bottom: 8), + child: Text( + AppLocalizations.of(context)!.realmNew, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + Expanded( + child: ListView( + children: [ + ListTile( + leading: const Icon(Icons.add), + title: Text(AppLocalizations.of(context)!.realmNewCreate), + onTap: () { + SolianRouter.router.pushNamed('realms.editor').then((did) { + if (did == true) { + onUpdate(); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + } + }); + }, + ), + ListTile( + leading: const Icon(Icons.travel_explore), + title: Text(AppLocalizations.of(context)!.realmNewJoin), + ), + ], + ), + ), + ], + ), + ); + } +}