✨ Personal page
This commit is contained in:
parent
083975bcd9
commit
f96ca899b5
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/moment_editor.dart';
|
||||||
import 'package:solian/screens/posts/screen.dart';
|
import 'package:solian/screens/posts/screen.dart';
|
||||||
import 'package:solian/screens/auth/signin.dart';
|
import 'package:solian/screens/auth/signin.dart';
|
||||||
|
import 'package:solian/screens/users/userinfo.dart';
|
||||||
import 'package:solian/utils/theme.dart';
|
import 'package:solian/utils/theme.dart';
|
||||||
import 'package:solian/widgets/empty.dart';
|
import 'package:solian/widgets/empty.dart';
|
||||||
import 'package:solian/widgets/layouts/two_column.dart';
|
import 'package:solian/widgets/layouts/two_column.dart';
|
||||||
@ -118,44 +119,50 @@ abstract class SolianRouter {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
pageBuilder: (context, state, child) => defaultPageBuilder(
|
pageBuilder: (context, state, child) => defaultPageBuilder(
|
||||||
context,
|
context,
|
||||||
state,
|
state,
|
||||||
SolianTheme.isLargeScreen(context)
|
SolianTheme.isLargeScreen(context)
|
||||||
? TwoColumnLayout(
|
? TwoColumnLayout(
|
||||||
sideChild: const AccountScreen(),
|
sideChild: const AccountScreen(),
|
||||||
mainChild: child,
|
mainChild: child,
|
||||||
)
|
)
|
||||||
: child,
|
: child,
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account',
|
path: '/account',
|
||||||
name: 'account',
|
name: 'account',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
!SolianTheme.isLargeScreen(context) ? const AccountScreen() : const PageEmptyWidget(),
|
!SolianTheme.isLargeScreen(context) ? const AccountScreen() : const PageEmptyWidget(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/sign-in',
|
path: '/auth/sign-in',
|
||||||
name: 'auth.sign-in',
|
name: 'auth.sign-in',
|
||||||
builder: (context, state) => SignInScreen(),
|
builder: (context, state) => SignInScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/sign-up',
|
path: '/auth/sign-up',
|
||||||
name: 'auth.sign-up',
|
name: 'auth.sign-up',
|
||||||
builder: (context, state) => SignUpScreen(),
|
builder: (context, state) => SignUpScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/friend',
|
path: '/account/friend',
|
||||||
name: 'account.friend',
|
name: 'account.friend',
|
||||||
builder: (context, state) => const FriendScreen(),
|
builder: (context, state) => const FriendScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/personalize',
|
path: '/account/personalize',
|
||||||
name: 'account.personalize',
|
name: 'account.personalize',
|
||||||
builder: (context, state) => const PersonalizeScreen(),
|
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/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/utils/theme.dart';
|
import 'package:solian/utils/theme.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/posts/comment_list.dart';
|
import 'package:solian/widgets/posts/comment_list.dart';
|
||||||
@ -172,9 +175,17 @@ class _PostItemState extends State<PostItem> {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AccountAvatar(
|
GestureDetector(
|
||||||
source: widget.item.author.avatar,
|
child: AccountAvatar(
|
||||||
direct: true,
|
source: widget.item.author.avatar,
|
||||||
|
direct: true,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
SolianRouter.router.pushNamed(
|
||||||
|
'users.info',
|
||||||
|
pathParameters: {'user': widget.item.author.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -203,9 +214,17 @@ class _PostItemState extends State<PostItem> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AccountAvatar(
|
GestureDetector(
|
||||||
source: widget.item.author.avatar,
|
child: AccountAvatar(
|
||||||
direct: true,
|
source: widget.item.author.avatar,
|
||||||
|
direct: true,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
SolianRouter.router.pushNamed(
|
||||||
|
'users.info',
|
||||||
|
pathParameters: {'user': widget.item.author.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -41,6 +41,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
|
window.flutterWebRenderer = "html";
|
||||||
|
|
||||||
window.addEventListener('load', function(ev) {
|
window.addEventListener('load', function(ev) {
|
||||||
// Download main.dart.js
|
// Download main.dart.js
|
||||||
_flutter.loader.loadEntrypoint({
|
_flutter.loader.loadEntrypoint({
|
||||||
|
Loading…
Reference in New Issue
Block a user