Realm discovery

This commit is contained in:
LittleSheep 2025-02-11 21:31:53 +08:00
parent f0a3bbe023
commit b3254e0f2f
8 changed files with 336 additions and 4 deletions

View File

@ -27,6 +27,7 @@
"screenChatNew": "New Channel", "screenChatNew": "New Channel",
"screenRealm": "Realm", "screenRealm": "Realm",
"screenRealmManage": "Edit Realm", "screenRealmManage": "Edit Realm",
"screenRealmDiscovery": "Realm Discovery",
"screenRealmNew": "New Realm", "screenRealmNew": "New Realm",
"screenNotification": "Notification", "screenNotification": "Notification",
"screenPostSearch": "Search Posts", "screenPostSearch": "Search Posts",
@ -619,5 +620,10 @@
"postQuestionAnswered": "Answered Question", "postQuestionAnswered": "Answered Question",
"postQuestionAnswerSelect": "Select as Answer", "postQuestionAnswerSelect": "Select as Answer",
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.", "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
"postVideoUpload": "Upload Video" "postVideoUpload": "Upload Video",
"realmJoin": "Join Realm",
"realmCommunityHint": "This realm is a community realm, you can freely join.",
"realmCommunityPublicChannelsHint": "The public channels in this realm",
"realmJoined": "Joined realm {}.",
"join": "Join"
} }

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天频道", "screenChatNew": "新建聊天频道",
"screenRealm": "领域", "screenRealm": "领域",
"screenRealmManage": "编辑领域", "screenRealmManage": "编辑领域",
"screenRealmDiscovery": "发现领域",
"screenRealmNew": "新建领域", "screenRealmNew": "新建领域",
"screenNotification": "通知", "screenNotification": "通知",
"screenPostSearch": "搜索帖子", "screenPostSearch": "搜索帖子",
@ -618,5 +619,10 @@
"postQuestionAnswerTitle": "精选解答", "postQuestionAnswerTitle": "精选解答",
"postQuestionAnswerSelect": "选择解答", "postQuestionAnswerSelect": "选择解答",
"postQuestionAnswerSelected": "解答已选择,奖励已发放。", "postQuestionAnswerSelected": "解答已选择,奖励已发放。",
"postVideoUpload": "上传视频" "postVideoUpload": "上传视频",
"realmJoin": "加入领域",
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
"realmJoined": "已加入领域 {}。",
"join": "加入"
} }

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天頻道", "screenChatNew": "新建聊天頻道",
"screenRealm": "領域", "screenRealm": "領域",
"screenRealmManage": "編輯領域", "screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域", "screenRealmNew": "新建領域",
"screenNotification": "通知", "screenNotification": "通知",
"screenPostSearch": "搜索帖子", "screenPostSearch": "搜索帖子",
@ -139,6 +140,7 @@
"writePostTypeStory": "發動態", "writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章", "writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題", "writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者", "fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!", "fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題", "fieldPostTitle": "標題",
@ -616,5 +618,11 @@
"postQuestionAnswered": "已解答的問題", "postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答", "postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答", "postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。" "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。",
"join": "加入"
} }

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天頻道", "screenChatNew": "新建聊天頻道",
"screenRealm": "領域", "screenRealm": "領域",
"screenRealmManage": "編輯領域", "screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域", "screenRealmNew": "新建領域",
"screenNotification": "通知", "screenNotification": "通知",
"screenPostSearch": "搜索帖子", "screenPostSearch": "搜索帖子",
@ -139,6 +140,7 @@
"writePostTypeStory": "發動態", "writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章", "writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題", "writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者", "fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!", "fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題", "fieldPostTitle": "標題",
@ -616,5 +618,11 @@
"postQuestionAnswered": "已解答的問題", "postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答", "postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答", "postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。" "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。",
"join": "加入"
} }

View File

@ -31,6 +31,7 @@ import 'package:surface/screens/post/post_search.dart';
import 'package:surface/screens/realm.dart'; import 'package:surface/screens/realm.dart';
import 'package:surface/screens/realm/manage.dart'; import 'package:surface/screens/realm/manage.dart';
import 'package:surface/screens/realm/realm_detail.dart'; import 'package:surface/screens/realm/realm_detail.dart';
import 'package:surface/screens/realm/realm_discovery.dart';
import 'package:surface/screens/settings.dart'; import 'package:surface/screens/settings.dart';
import 'package:surface/screens/sharing.dart'; import 'package:surface/screens/sharing.dart';
import 'package:surface/screens/wallet.dart'; import 'package:surface/screens/wallet.dart';
@ -199,6 +200,11 @@ final _appRoutes = [
editingRealmAlias: state.uri.queryParameters['editing'], editingRealmAlias: state.uri.queryParameters['editing'],
), ),
), ),
GoRoute(
path: '/discovery',
name: 'realmDiscovery',
builder: (context, state) => const RealmDiscoveryScreen(),
),
GoRoute( GoRoute(
path: '/:alias', path: '/:alias',
name: 'realmDetail', name: 'realmDetail',

View File

@ -100,6 +100,12 @@ class _RealmScreenState extends State<RealmScreen> {
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
actions: [ actions: [
IconButton(
icon: const Icon(Symbols.globe),
onPressed: () {
GoRouter.of(context).pushNamed('realmDiscovery');
},
),
IconButton( IconButton(
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module), icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () { onPressed: () {

View File

@ -0,0 +1,291 @@
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/providers/userinfo.dart';
import 'package:surface/types/chat.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/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
class RealmDiscoveryScreen extends StatefulWidget {
const RealmDiscoveryScreen({super.key});
@override
State<RealmDiscoveryScreen> createState() => _RealmDiscoveryScreenState();
}
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
List<SnRealm>? _realms;
bool _isBusy = false;
Future<void> _fetchRealms() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/realms');
_realms = List<SnRealm>.from(
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
);
} catch (err) {
if (mounted) context.showErrorDialog(err);
rethrow;
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchRealms();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return AppScaffold(
appBar: AppBar(
title: Text('screenRealmDiscovery').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: RefreshIndicator(
onRefresh: _fetchRealms,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _realms?.length ?? 0,
itemBuilder: (context, idx) {
final realm = _realms![idx];
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: 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: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: 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: () {
showModalBottomSheet(
context: context,
builder: (context) => _RealmJoinPopup(realm: realm),
);
},
),
),
).center();
},
),
),
),
],
),
);
}
}
class _RealmJoinPopup extends StatefulWidget {
final SnRealm realm;
const _RealmJoinPopup({required this.realm});
@override
State<_RealmJoinPopup> createState() => _RealmJoinPopupState();
}
class _RealmJoinPopupState extends State<_RealmJoinPopup> {
final List<String> _planJoinChannels = List.empty(growable: true);
List<SnChannel>? _channels;
bool _isBusy = false;
bool _isJoining = false;
Future<void> _fetchPublicChannels() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}');
final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
);
setState(() => _channels = out);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _joinRealm() async {
try {
setState(() => _isJoining = true);
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
'related': ua.user?.name,
});
await _joinSelectedChannels();
if (!mounted) return;
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
Navigator.pop(context);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isJoining = false);
}
}
Future<void> _joinSelectedChannels() async {
if (_planJoinChannels.isEmpty) return;
for (final channel in _planJoinChannels) {
try {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
'related': ua.user?.name,
});
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
}
@override
void initState() {
super.initState();
_fetchPublicChannels();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.group_add, size: 24),
const Gap(16),
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.realm.name,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
widget.realm.description,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
ElevatedButton(
onPressed: _isJoining ? null : () => _joinRealm(),
child: Text('join'.tr()),
),
],
).padding(horizontal: 24, bottom: 12),
const Divider(height: 1),
LoadingIndicator(isActive: _isBusy),
Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
Expanded(
child: ListView.builder(
itemCount: _channels?.length ?? 0,
itemBuilder: (context, index) {
final channel = _channels![index];
return CheckboxListTile(
value: _planJoinChannels.contains(channel.alias) ?? false,
title: Text(channel.name),
subtitle: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
secondary: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onChanged: (value) {
value ??= false;
if (value) {
setState(() => _planJoinChannels.add(channel.alias));
} else {
setState(() => _planJoinChannels.remove(channel.alias));
}
},
);
},
),
),
],
);
}
}

View File

@ -58,6 +58,7 @@ class AppScaffold extends StatelessWidget {
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand( body: SizedBox.expand(
child: AppBackground( child: AppBackground(
isRoot: true,
child: Column( child: Column(
children: [ children: [
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)), IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),