✨ Personal page
This commit is contained in:
		
							
								
								
									
										47
									
								
								lib/models/personal_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/models/personal_page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| class PersonalPage { | ||||
|   int id; | ||||
|   DateTime createdAt; | ||||
|   DateTime updatedAt; | ||||
|   DateTime? deletedAt; | ||||
|   String content; | ||||
|   String script; | ||||
|   String style; | ||||
|   Map<String, String>? links; | ||||
|   int accountId; | ||||
|  | ||||
|   PersonalPage({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|     this.deletedAt, | ||||
|     required this.content, | ||||
|     required this.script, | ||||
|     required this.style, | ||||
|     this.links, | ||||
|     required this.accountId, | ||||
|   }); | ||||
|  | ||||
|   factory PersonalPage.fromJson(Map<String, dynamic> json) => PersonalPage( | ||||
|         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, | ||||
|         content: json['content'], | ||||
|         script: json['script'], | ||||
|         style: json['style'], | ||||
|         links: json['links'], | ||||
|         accountId: json['account_id'], | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt?.toIso8601String(), | ||||
|         'content': content, | ||||
|         'script': script, | ||||
|         'style': style, | ||||
|         'links': links, | ||||
|         'account_id': accountId, | ||||
|       }; | ||||
| } | ||||
| @@ -19,6 +19,7 @@ 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/users/userinfo.dart'; | ||||
| import 'package:solian/utils/theme.dart'; | ||||
| import 'package:solian/widgets/empty.dart'; | ||||
| import 'package:solian/widgets/layouts/two_column.dart'; | ||||
| @@ -118,44 +119,50 @@ abstract class SolianRouter { | ||||
|         ], | ||||
|       ), | ||||
|       ShellRoute( | ||||
|           pageBuilder: (context, state, child) => defaultPageBuilder( | ||||
|                 context, | ||||
|                 state, | ||||
|                 SolianTheme.isLargeScreen(context) | ||||
|                     ? TwoColumnLayout( | ||||
|                         sideChild: const AccountScreen(), | ||||
|                         mainChild: child, | ||||
|                       ) | ||||
|                     : child, | ||||
|               ), | ||||
|           routes: [ | ||||
|             GoRoute( | ||||
|               path: '/account', | ||||
|               name: 'account', | ||||
|               builder: (context, state) => | ||||
|                   !SolianTheme.isLargeScreen(context) ? const AccountScreen() : const PageEmptyWidget(), | ||||
|             ), | ||||
|             GoRoute( | ||||
|               path: '/auth/sign-in', | ||||
|               name: 'auth.sign-in', | ||||
|               builder: (context, state) => SignInScreen(), | ||||
|             ), | ||||
|             GoRoute( | ||||
|               path: '/auth/sign-up', | ||||
|               name: 'auth.sign-up', | ||||
|               builder: (context, state) => SignUpScreen(), | ||||
|             ), | ||||
|             GoRoute( | ||||
|               path: '/account/friend', | ||||
|               name: 'account.friend', | ||||
|               builder: (context, state) => const FriendScreen(), | ||||
|             ), | ||||
|             GoRoute( | ||||
|               path: '/account/personalize', | ||||
|               name: 'account.personalize', | ||||
|               builder: (context, state) => const PersonalizeScreen(), | ||||
|             ), | ||||
|           ]), | ||||
|         pageBuilder: (context, state, child) => defaultPageBuilder( | ||||
|           context, | ||||
|           state, | ||||
|           SolianTheme.isLargeScreen(context) | ||||
|               ? TwoColumnLayout( | ||||
|                   sideChild: const AccountScreen(), | ||||
|                   mainChild: child, | ||||
|                 ) | ||||
|               : child, | ||||
|         ), | ||||
|         routes: [ | ||||
|           GoRoute( | ||||
|             path: '/account', | ||||
|             name: 'account', | ||||
|             builder: (context, state) => | ||||
|                 !SolianTheme.isLargeScreen(context) ? const AccountScreen() : const PageEmptyWidget(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/auth/sign-in', | ||||
|             name: 'auth.sign-in', | ||||
|             builder: (context, state) => SignInScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/auth/sign-up', | ||||
|             name: 'auth.sign-up', | ||||
|             builder: (context, state) => SignUpScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/account/friend', | ||||
|             name: 'account.friend', | ||||
|             builder: (context, state) => const FriendScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/account/personalize', | ||||
|             name: 'account.personalize', | ||||
|             builder: (context, state) => const PersonalizeScreen(), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/users/:user', | ||||
|         name: 'users.info', | ||||
|         builder: (context, state) => UserInfoScreen(name: state.pathParameters['user'] as String), | ||||
|       ), | ||||
|     ], | ||||
|   ); | ||||
|  | ||||
|   | ||||
							
								
								
									
										162
									
								
								lib/screens/users/userinfo.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								lib/screens/users/userinfo.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/personal_page.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/utils/theme.dart'; | ||||
| import 'package:solian/widgets/account/account_avatar.dart'; | ||||
| import 'package:solian/widgets/account/personal_page_content.dart'; | ||||
| import 'package:solian/widgets/exts.dart'; | ||||
| import 'package:solian/widgets/scaffold.dart'; | ||||
|  | ||||
| class UserInfoScreen extends StatefulWidget { | ||||
|   final String name; | ||||
|  | ||||
|   const UserInfoScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   State<UserInfoScreen> createState() => _UserInfoScreenState(); | ||||
| } | ||||
|  | ||||
| class _UserInfoScreenState extends State<UserInfoScreen> { | ||||
|   final _client = Client(); | ||||
|  | ||||
|   Account? _userinfo; | ||||
|   PersonalPage? _page; | ||||
|  | ||||
|   Future<Account> fetchUserinfo() async { | ||||
|     final res = await Future.wait([ | ||||
|       _client.get(getRequestUri('passport', '/api/users/${widget.name}')), | ||||
|       _client.get(getRequestUri('passport', '/api/users/${widget.name}/page')) | ||||
|     ], eagerError: true); | ||||
|     final mistakeRes = res.indexWhere((x) => x.statusCode != 200); | ||||
|     if (mistakeRes > -1) { | ||||
|       var message = utf8.decode(res[0].bodyBytes); | ||||
|       context.showErrorDialog(message); | ||||
|       throw Exception(message); | ||||
|     } else { | ||||
|       final info = Account.fromJson(jsonDecode(utf8.decode(res[0].bodyBytes))); | ||||
|       final page = PersonalPage.fromJson(jsonDecode(utf8.decode(res[1].bodyBytes))); | ||||
|       setState(() { | ||||
|         _userinfo = info; | ||||
|         _page = page; | ||||
|       }); | ||||
|       return info; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   String getAuthorDescribe() => _userinfo!.description.isNotEmpty ? _userinfo!.description : 'No description yet.'; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return IndentScaffold( | ||||
|       title: _userinfo?.nick ?? 'Loading...', | ||||
|       fixedAppBarColor: SolianTheme.isLargeScreen(context), | ||||
|       hideDrawer: true, | ||||
|       noSafeArea: true, | ||||
|       child: FutureBuilder( | ||||
|         future: fetchUserinfo(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (!snapshot.hasData || snapshot.data == null) { | ||||
|             return const Center(child: CircularProgressIndicator()); | ||||
|           } | ||||
|  | ||||
|           return ListView( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             children: [ | ||||
|               const SizedBox(height: 8), | ||||
|               ClipRRect( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 child: AspectRatio( | ||||
|                   aspectRatio: 16 / 5, | ||||
|                   child: Container( | ||||
|                     color: Theme.of(context).colorScheme.surfaceVariant, | ||||
|                     child: _userinfo?.banner != null | ||||
|                         ? CachedNetworkImage( | ||||
|                             imageUrl: getRequestUri('passport', '/api/avatar/${_userinfo!.banner}').toString(), | ||||
|                             fit: BoxFit.cover, | ||||
|                             progressIndicatorBuilder: (_, __, DownloadProgress loadingProgress) { | ||||
|                               return Center( | ||||
|                                 child: CircularProgressIndicator( | ||||
|                                   value: loadingProgress.totalSize != null | ||||
|                                       ? loadingProgress.downloaded / loadingProgress.totalSize! | ||||
|                                       : null, | ||||
|                                 ), | ||||
|                               ); | ||||
|                             }, | ||||
|                           ) | ||||
|                         : Container(), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(left: 16, right: 16, top: 20), | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     AccountAvatar(source: _userinfo!.avatar, radius: 32), | ||||
|                     const SizedBox(width: 12), | ||||
|                     Expanded( | ||||
|                       child: Column( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Row( | ||||
|                             textBaseline: TextBaseline.alphabetic, | ||||
|                             crossAxisAlignment: CrossAxisAlignment.baseline, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 _userinfo!.nick, | ||||
|                                 maxLines: 1, | ||||
|                                 style: const TextStyle( | ||||
|                                   fontSize: 20, | ||||
|                                   fontWeight: FontWeight.bold, | ||||
|                                   overflow: TextOverflow.ellipsis, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               const SizedBox(width: 6), | ||||
|                               Text( | ||||
|                                 '@${_userinfo!.name}', | ||||
|                                 maxLines: 1, | ||||
|                                 style: const TextStyle( | ||||
|                                   fontSize: 16, | ||||
|                                   fontWeight: FontWeight.w400, | ||||
|                                   overflow: TextOverflow.ellipsis, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           Text( | ||||
|                             _userinfo!.description, | ||||
|                             maxLines: 3, | ||||
|                             style: const TextStyle( | ||||
|                               fontSize: 14, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|               const Padding( | ||||
|                 padding: EdgeInsets.symmetric(vertical: 8), | ||||
|                 child: Divider(thickness: 0.3, indent: 4, endIndent: 4), | ||||
|               ), | ||||
|               ClipRRect( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 child: Padding( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                   child: PersonalPageContent(item: _page!), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										28
									
								
								lib/widgets/account/personal_page_content.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								lib/widgets/account/personal_page_content.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:solian/models/personal_page.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class PersonalPageContent extends StatelessWidget { | ||||
|   final PersonalPage item; | ||||
|  | ||||
|   const PersonalPageContent({super.key, required this.item}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Markdown( | ||||
|       selectable: true, | ||||
|       data: item.content, | ||||
|       shrinkWrap: true, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
|       padding: const EdgeInsets.all(0), | ||||
|       onTapLink: (text, href, title) async { | ||||
|         if (href == null) return; | ||||
|         await launchUrlString( | ||||
|           href, | ||||
|           mode: LaunchMode.externalApplication, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| 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'; | ||||
| import 'package:solian/utils/theme.dart'; | ||||
| import 'package:solian/widgets/account/account_avatar.dart'; | ||||
| import 'package:solian/widgets/posts/comment_list.dart'; | ||||
| @@ -172,9 +175,17 @@ class _PostItemState extends State<PostItem> { | ||||
|             Row( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 AccountAvatar( | ||||
|                   source: widget.item.author.avatar, | ||||
|                   direct: true, | ||||
|                 GestureDetector( | ||||
|                   child: AccountAvatar( | ||||
|                     source: widget.item.author.avatar, | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                   onTap: () { | ||||
|                     SolianRouter.router.pushNamed( | ||||
|                       'users.info', | ||||
|                       pathParameters: {'user': widget.item.author.name}, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: Column( | ||||
| @@ -203,9 +214,17 @@ class _PostItemState extends State<PostItem> { | ||||
|             child: Row( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 AccountAvatar( | ||||
|                   source: widget.item.author.avatar, | ||||
|                   direct: true, | ||||
|                 GestureDetector( | ||||
|                   child: AccountAvatar( | ||||
|                     source: widget.item.author.avatar, | ||||
|                     direct: true, | ||||
|                   ), | ||||
|                   onTap: () { | ||||
|                     SolianRouter.router.pushNamed( | ||||
|                       'users.info', | ||||
|                       pathParameters: {'user': widget.item.author.name}, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: Column( | ||||
|   | ||||
| @@ -41,6 +41,8 @@ | ||||
| </head> | ||||
| <body> | ||||
|   <script> | ||||
|     window.flutterWebRenderer = "html"; | ||||
|  | ||||
|     window.addEventListener('load', function(ev) { | ||||
|       // Download main.dart.js | ||||
|       _flutter.loader.loadEntrypoint({ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user