Realms creation

This commit is contained in:
LittleSheep 2024-05-28 22:13:23 +08:00
parent 99f3211151
commit c50a49f37d
8 changed files with 514 additions and 97 deletions

View File

@ -11,7 +11,7 @@ class RealmProvider extends GetxController {
client.httpClient.baseUrl = ServiceFinder.services['passport']; client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator); client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.get('/realms/me/available'); final resp = await client.get('/api/realms/me/available');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.bodyString);
} }

View File

@ -8,6 +8,8 @@ import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/contact.dart'; import 'package:solian/screens/contact.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/social.dart'; import 'package:solian/screens/social.dart';
import 'package:solian/screens/posts/post_publish.dart'; import 'package:solian/screens/posts/post_publish.dart';
import 'package:solian/shells/basic_shell.dart'; import 'package:solian/shells/basic_shell.dart';
@ -30,6 +32,11 @@ abstract class AppRouter {
name: 'contact', name: 'contact',
builder: (context, state) => const ContactScreen(), builder: (context, state) => const ContactScreen(),
), ),
GoRoute(
path: '/realms',
name: 'realms',
builder: (context, state) => const RealmListScreen(),
),
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
@ -114,6 +121,16 @@ abstract class AppRouter {
); );
}, },
), ),
GoRoute(
path: '/realm/organize',
name: 'realmOrganizing',
builder: (context, state) {
final arguments = state.extra as RealmOrganizeArguments?;
return RealmOrganizeScreen(
edit: arguments?.edit,
);
},
),
], ],
); );
} }

View File

@ -52,105 +52,104 @@ class _ContactScreenState extends State<ContactScreen> {
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: FutureBuilder( child: FutureBuilder(
future: auth.isAuthorized, future: auth.isAuthorized,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} else if (snapshot.data == false) { } else if (snapshot.data == false) {
return SigninRequiredOverlay( return SigninRequiredOverlay(
onSignedIn: () { onSignedIn: () {
getChannels(); getChannels();
}, },
); );
} }
return SafeArea( return SafeArea(
child: NestedScrollView( child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) { headerSliverBuilder: (context, innerBoxIsScrolled) {
return [ return [
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context), context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: Text('contact'.tr), title: Text('contact'.tr),
centerTitle: false, centerTitle: false,
titleSpacing: titleSpacing:
SolianTheme.isLargeScreen(context) ? null : 24, SolianTheme.isLargeScreen(context) ? null : 24,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
actions: [ actions: [
const NotificationButton(), const NotificationButton(),
IconButton( IconButton(
icon: const Icon(Icons.add_circle), icon: const Icon(Icons.add_circle),
onPressed: () { onPressed: () {
AppRouter.instance AppRouter.instance
.pushNamed('channelOrganizing') .pushNamed('channelOrganizing')
.then( .then(
(value) { (value) {
if (value != null) { if (value != null) getChannels();
getChannels(); },
} );
}, },
); ),
}, SizedBox(
), width: SolianTheme.isLargeScreen(context) ? 8 : 16,
SizedBox( ),
width: SolianTheme.isLargeScreen(context) ? 8 : 16, ],
),
],
),
), ),
]; ),
}, ];
body: MediaQuery.removePadding( },
removeTop: true, body: MediaQuery.removePadding(
context: context, removeTop: true,
child: Column( context: context,
children: [ child: Column(
if (_isBusy) children: [
const LinearProgressIndicator().animate().scaleX(), if (_isBusy)
Expanded( const LinearProgressIndicator().animate().scaleX(),
child: RefreshIndicator( Expanded(
onRefresh: () => getChannels(), child: RefreshIndicator(
child: ListView.builder( onRefresh: () => getChannels(),
itemCount: _channels.length, child: ListView.builder(
itemBuilder: (context, index) { itemCount: _channels.length,
final element = _channels[index]; itemBuilder: (context, index) {
return ListTile( final element = _channels[index];
leading: CircleAvatar( return ListTile(
backgroundColor: Colors.indigo, leading: CircleAvatar(
child: FaIcon( backgroundColor: Colors.indigo,
element.icon, child: FaIcon(
color: Colors.white, element.icon,
size: 16, color: Colors.white,
), size: 16,
), ),
contentPadding: ),
const EdgeInsets.symmetric(horizontal: 24), contentPadding:
title: Text(element.name), const EdgeInsets.symmetric(horizontal: 24),
subtitle: Text(element.description), title: Text(element.name),
onTap: () { subtitle: Text(element.description),
AppRouter.instance.pushNamed( onTap: () {
'channelChat', AppRouter.instance.pushNamed(
pathParameters: {'alias': element.alias}, 'channelChat',
queryParameters: { pathParameters: {'alias': element.alias},
if (element.realmId != null) queryParameters: {
'realm': element.realm!.alias, if (element.realmId != null)
}, 'realm': element.realm!.alias,
); },
}, );
); },
}, );
), },
), ),
), ),
], ),
), ],
), ),
), ),
); ),
}), );
},
),
); );
} }
} }

View File

@ -1,10 +1,183 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
class RealmListScreen extends StatelessWidget { class RealmListScreen extends StatefulWidget {
const RealmListScreen({super.key}); const RealmListScreen({super.key});
@override
State<RealmListScreen> createState() => _RealmListScreenState();
}
class _RealmListScreenState extends State<RealmListScreen> {
bool _isBusy = true;
final List<Realm> _realms = List.empty(growable: true);
getRealms() async {
setState(() => _isBusy = true);
final RealmProvider provider = Get.find();
final resp = await provider.listAvailableRealm();
setState(() {
_realms.clear();
_realms.addAll(
resp.body.map((e) => Realm.fromJson(e)).toList().cast<Realm>(),
);
});
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
getRealms();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
throw UnimplementedError(); final AuthProvider auth = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
child: FutureBuilder(
future: auth.isAuthorized,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.data == false) {
return SigninRequiredOverlay(
onSignedIn: () {
getRealms();
},
);
}
return SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver: SliverAppBar(
title: Text('realm'.tr),
centerTitle: false,
titleSpacing:
SolianTheme.isLargeScreen(context) ? null : 24,
forceElevated: innerBoxIsScrolled,
actions: [
const NotificationButton(),
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: () {
AppRouter.instance
.pushNamed('realmOrganizing')
.then(
(value) {
if (value != null) getRealms();
},
);
},
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
),
];
},
body: MediaQuery.removePadding(
removeTop: true,
context: context,
child: Column(
children: [
if (_isBusy)
const LinearProgressIndicator().animate().scaleX(),
Expanded(
child: RefreshIndicator(
onRefresh: () => getRealms(),
child: ListView.builder(
itemCount: _realms.length,
itemBuilder: (context, index) {
final element = _realms[index];
return buildRealm(element);
},
),
),
),
],
),
),
),
);
},
),
);
}
Widget buildRealm(Realm element) {
return Card(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
),
const Positioned(
bottom: -30,
left: 18,
child: CircleAvatar(
radius: 24,
backgroundColor: Colors.indigo,
child: FaIcon(
FontAwesomeIcons.globe,
color: Colors.white,
size: 18,
),
),
),
],
),
).paddingOnly(bottom: 20),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(element.name),
subtitle: Text(
element.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
),
onTap: () {},
),
),
).paddingOnly(left: 8, right: 8, bottom: 4);
} }
} }

View File

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/prev_page.dart';
import 'package:uuid/uuid.dart';
class RealmOrganizeArguments {
final Realm? edit;
RealmOrganizeArguments({this.edit});
}
class RealmOrganizeScreen extends StatefulWidget {
final Realm? edit;
const RealmOrganizeScreen({super.key, this.edit});
@override
State<RealmOrganizeScreen> createState() => _RealmOrganizeScreenState();
}
class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
bool _isBusy = false;
final _aliasController = TextEditingController();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
bool _isCommunity = false;
bool _isPublic = false;
void applyRealm() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (_aliasController.value.text.isEmpty) randomizeAlias();
setState(() => _isBusy = true);
final client = GetConnect(maxAuthRetries: 3);
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final payload = {
'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text,
'description': _descriptionController.value.text,
'is_public': _isPublic,
'is_community': _isCommunity,
};
Response resp;
if (widget.edit != null) {
resp = await client.put('/api/realms/${widget.edit!.id}', payload);
} else {
resp = await client.post('/api/realms', payload);
}
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
AppRouter.instance.pop(resp.body);
}
setState(() => _isBusy = false);
}
void randomizeAlias() {
_aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12);
}
void syncWidget() {
if (widget.edit != null) {
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description;
_isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity;
}
}
void cancelAction() {
AppRouter.instance.pop();
}
@override
void initState() {
syncWidget();
super.initState();
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
title: Text('realmOrganizing'.tr),
leading: const PrevPageButton(),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyRealm(),
child: Text('apply'.tr.toUpperCase()),
)
],
),
body: SafeArea(
top: false,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null)
MaterialBanner(
leading: const Icon(Icons.edit),
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text(
'realmEditingNotify'
.trParams({'realm': '#${widget.edit!.alias}'}),
),
actions: [
TextButton(
onPressed: cancelAction,
child: Text('cancel'.tr),
),
],
),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _aliasController,
decoration: InputDecoration.collapsed(
hintText: 'realmAlias'.tr,
),
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),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
const Divider(thickness: 0.3),
TextField(
autocorrect: true,
controller: _nameController,
decoration: InputDecoration.collapsed(
hintText: 'realmName'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 8),
const Divider(thickness: 0.3),
Expanded(
child: TextField(
minLines: 5,
maxLines: null,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'realmDescription'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 12),
),
const Divider(thickness: 0.3),
CheckboxListTile(
title: Text('realmPublic'.tr),
value: _isPublic,
onChanged: (newValue) =>
setState(() => _isPublic = newValue ?? false),
controlAffinity: ListTileControlAffinity.leading,
),
CheckboxListTile(
title: Text('realmCommunity'.tr),
value: _isCommunity,
onChanged: (newValue) =>
setState(() => _isCommunity = newValue ?? false),
controlAffinity: ListTileControlAffinity.leading,
),
],
),
),
),
);
}
}

View File

@ -96,6 +96,14 @@ class SolianMessages extends Translations {
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
'realm': 'Realm',
'realms': 'Realms',
'realmOrganizing': 'Organize a realm',
'realmAlias': 'Alias (Identifier)',
'realmName': 'Name',
'realmDescription': 'Description',
'realmPublic': 'Public Realm',
'realmCommunity': 'Community Realm',
'channelOrganizing': 'Organize a channel', 'channelOrganizing': 'Organize a channel',
'channelEditingNotify': 'You\'re editing channel @channel', 'channelEditingNotify': 'You\'re editing channel @channel',
'channelAlias': 'Alias (Identifier)', 'channelAlias': 'Alias (Identifier)',
@ -205,6 +213,14 @@ class SolianMessages extends Translations {
'attachmentAddFile': '附加文件', 'attachmentAddFile': '附加文件',
'attachmentSetting': '调整附件', 'attachmentSetting': '调整附件',
'attachmentAlt': '替代文字', 'attachmentAlt': '替代文字',
'realm': '领域',
'realms': '领域',
'realmOrganizing': '组织领域',
'realmAlias': '别称(标识符)',
'realmName': '显示名称',
'realmDescription': '领域简介',
'realmPublic': '公开领域',
'realmCommunity': '社区领域',
'channelOrganizing': '组织频道', 'channelOrganizing': '组织频道',
'channelEditingNotify': '你正在编辑频道 @channel', 'channelEditingNotify': '你正在编辑频道 @channel',
'channelAlias': '别称(标识符)', 'channelAlias': '别称(标识符)',

View File

@ -13,6 +13,11 @@ abstract class AppNavigation {
label: 'contact'.tr, label: 'contact'.tr,
page: 'contact', page: 'contact',
), ),
AppNavigationDestination(
icon: const Icon(Icons.workspaces),
label: 'realms'.tr,
page: 'realms',
),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.account_circle), icon: const Icon(Icons.account_circle),
label: 'account'.tr, label: 'account'.tr,
@ -26,6 +31,9 @@ class AppNavigationDestination {
final String label; final String label;
final String page; final String page;
AppNavigationDestination( AppNavigationDestination({
{required this.icon, required this.label, required this.page}); required this.icon,
required this.label,
required this.page,
});
} }

View File

@ -23,6 +23,7 @@ class _AppNavigationBottomBarState extends State<AppNavigationBottomBar> {
), ),
) )
.toList(), .toList(),
type: BottomNavigationBarType.fixed,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered, landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
showUnselectedLabels: false, showUnselectedLabels: false,