✨ Dashboard explore
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
{
|
||||
"solian": "HAIYAA",
|
||||
"solian": "索链",
|
||||
"explore": "探索"
|
||||
}
|
@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/providers/layout_provider.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/utils/timeago.dart';
|
||||
import 'package:solian/widgets/wrapper.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
initTimeAgo();
|
||||
|
||||
runApp(const SolianApp());
|
||||
}
|
||||
|
||||
@ -28,8 +31,10 @@ class SolianApp extends StatelessWidget {
|
||||
return Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Provider(
|
||||
create: (_) => LayoutConfig(context),
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider(create: (_) => LayoutConfig(context))
|
||||
],
|
||||
child: LayoutWrapper(
|
||||
child: child,
|
||||
),
|
||||
|
59
lib/models/author.dart
Executable file
59
lib/models/author.dart
Executable file
@ -0,0 +1,59 @@
|
||||
class Author {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String name;
|
||||
String nick;
|
||||
String avatar;
|
||||
String banner;
|
||||
String description;
|
||||
String emailAddress;
|
||||
int powerLevel;
|
||||
int externalId;
|
||||
|
||||
Author({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
required this.description,
|
||||
required this.emailAddress,
|
||||
required this.powerLevel,
|
||||
required this.externalId,
|
||||
});
|
||||
|
||||
factory Author.fromJson(Map<String, dynamic> json) => Author(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
name: json["name"],
|
||||
nick: json["nick"],
|
||||
avatar: json["avatar"],
|
||||
banner: json["banner"],
|
||||
description: json["description"],
|
||||
emailAddress: json["email_address"],
|
||||
powerLevel: json["power_level"],
|
||||
externalId: json["external_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"name": name,
|
||||
"nick": nick,
|
||||
"avatar": avatar,
|
||||
"banner": banner,
|
||||
"description": description,
|
||||
"email_address": emailAddress,
|
||||
"power_level": powerLevel,
|
||||
"external_id": externalId,
|
||||
};
|
||||
}
|
79
lib/models/notification.dart
Executable file
79
lib/models/notification.dart
Executable file
@ -0,0 +1,79 @@
|
||||
class Notification {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String subject;
|
||||
String content;
|
||||
List<Link>? links;
|
||||
bool isImportant;
|
||||
DateTime? readAt;
|
||||
int senderId;
|
||||
int recipientId;
|
||||
|
||||
Notification({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.subject,
|
||||
required this.content,
|
||||
this.links,
|
||||
required this.isImportant,
|
||||
this.readAt,
|
||||
required this.senderId,
|
||||
required this.recipientId,
|
||||
});
|
||||
|
||||
factory Notification.fromJson(Map<String, dynamic> json) => Notification(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
subject: json["subject"],
|
||||
content: json["content"],
|
||||
links: json["links"] != null
|
||||
? List<Link>.from(json["links"].map((x) => Link.fromJson(x)))
|
||||
: List.empty(),
|
||||
isImportant: json["is_important"],
|
||||
readAt: json["read_at"],
|
||||
senderId: json["sender_id"],
|
||||
recipientId: json["recipient_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
"links": links != null
|
||||
? List<dynamic>.from(links!.map((x) => x.toJson()))
|
||||
: List.empty(),
|
||||
"is_important": isImportant,
|
||||
"read_at": readAt,
|
||||
"sender_id": senderId,
|
||||
"recipient_id": recipientId,
|
||||
};
|
||||
}
|
||||
|
||||
class Link {
|
||||
String label;
|
||||
String url;
|
||||
|
||||
Link({
|
||||
required this.label,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
factory Link.fromJson(Map<String, dynamic> json) => Link(
|
||||
label: json["label"],
|
||||
url: json["url"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"label": label,
|
||||
"url": url,
|
||||
};
|
||||
}
|
17
lib/models/pagination.dart
Executable file
17
lib/models/pagination.dart
Executable file
@ -0,0 +1,17 @@
|
||||
class PaginationResult {
|
||||
int count;
|
||||
List<dynamic>? data;
|
||||
|
||||
PaginationResult({
|
||||
required this.count,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory PaginationResult.fromJson(Map<String, dynamic> json) =>
|
||||
PaginationResult(count: json["count"], data: json["data"]);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"count": count,
|
||||
"data": data,
|
||||
};
|
||||
}
|
154
lib/models/post.dart
Executable file
154
lib/models/post.dart
Executable file
@ -0,0 +1,154 @@
|
||||
import 'package:solian/models/author.dart';
|
||||
|
||||
class Post {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String alias;
|
||||
String title;
|
||||
String description;
|
||||
String content;
|
||||
String modelType;
|
||||
int commentCount;
|
||||
int reactionCount;
|
||||
int authorId;
|
||||
int? realmId;
|
||||
Author author;
|
||||
List<Attachment>? attachments;
|
||||
Map<String, dynamic>? reactionList;
|
||||
|
||||
Post({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.alias,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.content,
|
||||
required this.modelType,
|
||||
required this.commentCount,
|
||||
required this.reactionCount,
|
||||
required this.authorId,
|
||||
this.realmId,
|
||||
required this.author,
|
||||
this.attachments,
|
||||
this.reactionList,
|
||||
});
|
||||
|
||||
factory Post.fromJson(Map<String, dynamic> json) => Post(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
alias: json["alias"],
|
||||
title: json["title"],
|
||||
description: json["description"],
|
||||
content: json["content"],
|
||||
modelType: json["model_type"],
|
||||
commentCount: json["comment_count"],
|
||||
reactionCount: json["reaction_count"],
|
||||
authorId: json["author_id"],
|
||||
realmId: json["realm_id"],
|
||||
author: Author.fromJson(json["author"]),
|
||||
attachments: json["attachments"] != null
|
||||
? List<Attachment>.from(
|
||||
json["attachments"].map((x) => Attachment.fromJson(x)))
|
||||
: List.empty(),
|
||||
reactionList: json["reaction_list"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"alias": alias,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"content": content,
|
||||
"model_type": modelType,
|
||||
"comment_count": commentCount,
|
||||
"reaction_count": reactionCount,
|
||||
"author_id": authorId,
|
||||
"realm_id": realmId,
|
||||
"author": author.toJson(),
|
||||
"attachments": attachments == null
|
||||
? List.empty()
|
||||
: List<dynamic>.from(attachments!.map((x) => x.toJson())),
|
||||
"reaction_list": reactionList,
|
||||
};
|
||||
}
|
||||
|
||||
class Attachment {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String fileId;
|
||||
int filesize;
|
||||
String filename;
|
||||
String mimetype;
|
||||
int type;
|
||||
String externalUrl;
|
||||
Author author;
|
||||
int? articleId;
|
||||
int? momentId;
|
||||
int? commentId;
|
||||
int? authorId;
|
||||
|
||||
Attachment({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.fileId,
|
||||
required this.filesize,
|
||||
required this.filename,
|
||||
required this.mimetype,
|
||||
required this.type,
|
||||
required this.externalUrl,
|
||||
required this.author,
|
||||
this.articleId,
|
||||
this.momentId,
|
||||
this.commentId,
|
||||
this.authorId,
|
||||
});
|
||||
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
fileId: json["file_id"],
|
||||
filesize: json["filesize"],
|
||||
filename: json["filename"],
|
||||
mimetype: json["mimetype"],
|
||||
type: json["type"],
|
||||
externalUrl: json["external_url"],
|
||||
author: Author.fromJson(json["author"]),
|
||||
articleId: json["article_id"],
|
||||
momentId: json["moment_id"],
|
||||
commentId: json["comment_id"],
|
||||
authorId: json["author_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"file_id": fileId,
|
||||
"filesize": filesize,
|
||||
"filename": filename,
|
||||
"mimetype": mimetype,
|
||||
"type": type,
|
||||
"external_url": externalUrl,
|
||||
"author": author.toJson(),
|
||||
"article_id": articleId,
|
||||
"moment_id": momentId,
|
||||
"comment_id": commentId,
|
||||
"author_id": authorId,
|
||||
};
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/providers/layout_provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:solian/widgets/posts/item.dart';
|
||||
|
||||
class ExploreScreen extends StatefulWidget {
|
||||
const ExploreScreen({super.key});
|
||||
@ -11,19 +19,67 @@ class ExploreScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExploreScreenState extends State<ExploreScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
Future<void> fetchFeed(int pageKey) async {
|
||||
final offset = pageKey;
|
||||
const take = 5;
|
||||
|
||||
var uri =
|
||||
getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
|
||||
|
||||
var res = await _client.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result =
|
||||
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items =
|
||||
result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
|
||||
final isLastPage = (result.count - pageKey) < take;
|
||||
if (isLastPage || result.data == null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
} else {
|
||||
final nextPageKey = pageKey + items.length;
|
||||
_pagingController.appendPage(items, nextPageKey);
|
||||
}
|
||||
} else {
|
||||
_pagingController.error = utf8.decode(res.bodyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
context.read<LayoutConfig>().title = AppLocalizations.of(context)!.explore;
|
||||
// Wait for the context
|
||||
context.read<LayoutConfig>().title =
|
||||
AppLocalizations.of(context)!.explore;
|
||||
});
|
||||
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener((pageKey) => fetchFeed(pageKey));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text("Woah"),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: PagedListView<int, Post>.separated(
|
||||
pagingController: _pagingController,
|
||||
separatorBuilder: (context, index) => const Divider(thickness: 0.3),
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) => PostItem(item: item),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
10
lib/utils/service_url.dart
Normal file
10
lib/utils/service_url.dart
Normal file
@ -0,0 +1,10 @@
|
||||
const serviceUrls = {
|
||||
'passport': 'https://id.solsynth.dev',
|
||||
'interactive': 'https://co.solsynth.dev',
|
||||
'messaging': 'https://im.solsynth.dev'
|
||||
};
|
||||
|
||||
Uri getRequestUri(String service, String path) {
|
||||
final baseUrl = serviceUrls[service];
|
||||
return Uri.parse(baseUrl! + path);
|
||||
}
|
5
lib/utils/timeago.dart
Normal file
5
lib/utils/timeago.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
void initTimeAgo() {
|
||||
timeago.setLocaleMessages('zh', timeago.ZhMessages());
|
||||
}
|
34
lib/widgets/posts/attachment_screen.dart
Executable file
34
lib/widgets/posts/attachment_screen.dart
Executable file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AttachmentScreen extends StatelessWidget {
|
||||
final String tag;
|
||||
final String url;
|
||||
|
||||
const AttachmentScreen({super.key, required this.tag, required this.url});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InteractiveViewer(
|
||||
boundaryMargin: const EdgeInsets.all(128),
|
||||
minScale: 0.1,
|
||||
maxScale: 16.0,
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Image.network(url, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
59
lib/widgets/posts/content/article.dart
Normal file
59
lib/widgets/posts/content/article.dart
Normal file
@ -0,0 +1,59 @@
|
||||
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:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ArticleContent extends StatelessWidget {
|
||||
final Post item;
|
||||
final bool brief;
|
||||
|
||||
const ArticleContent({super.key, required this.item, required this.brief});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return brief
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
item.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(item.title),
|
||||
subtitle: Text(item.description),
|
||||
),
|
||||
const Divider(color: Color(0xffefefef)),
|
||||
Markdown(
|
||||
selectable: !brief,
|
||||
data: item.content,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
131
lib/widgets/posts/content/attachment.dart
Normal file
131
lib/widgets/posts/content/attachment.dart
Normal file
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
||||
import 'package:solian/widgets/posts/attachment_screen.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class AttachmentItem extends StatefulWidget {
|
||||
final Attachment item;
|
||||
|
||||
const AttachmentItem({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<AttachmentItem> createState() => _AttachmentItemState();
|
||||
}
|
||||
|
||||
class _AttachmentItemState extends State<AttachmentItem> {
|
||||
String getTag() => 'attachment-${widget.item.fileId}';
|
||||
|
||||
Uri getFileUri() =>
|
||||
getRequestUri('interactive', '/api/attachments/o/${widget.item.fileId}');
|
||||
|
||||
VideoPlayerController? _vpController;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const borderRadius = Radius.circular(16);
|
||||
|
||||
Widget content;
|
||||
|
||||
if (widget.item.type == 1) {
|
||||
content = GestureDetector(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Hero(
|
||||
tag: getTag(),
|
||||
child: Image.network(
|
||||
getFileUri().toString(),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) {
|
||||
return AttachmentScreen(
|
||||
tag: getTag(),
|
||||
url: getFileUri().toString(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_vpController = VideoPlayerController.networkUrl(getFileUri());
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _vpController!,
|
||||
);
|
||||
|
||||
content = FutureBuilder(
|
||||
future: () async {
|
||||
await _vpController?.initialize();
|
||||
return true;
|
||||
}(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Chewie(
|
||||
controller: _chewieController!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.9,
|
||||
),
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vpController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentList extends StatelessWidget {
|
||||
final List<Attachment> items;
|
||||
|
||||
const AttachmentList({super.key, required this.items});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlutterCarousel(
|
||||
options: CarouselOptions(
|
||||
aspectRatio: 16 / 9,
|
||||
showIndicator: true,
|
||||
slideIndicator: const CircularSlideIndicator(),
|
||||
),
|
||||
items: items.map((item) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: AttachmentItem(item: item),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
21
lib/widgets/posts/content/moment.dart
Normal file
21
lib/widgets/posts/content/moment.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
|
||||
class MomentContent extends StatelessWidget {
|
||||
final Post item;
|
||||
final bool brief;
|
||||
|
||||
const MomentContent({super.key, required this.brief, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Markdown(
|
||||
selectable: !brief,
|
||||
data: item.content,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
);
|
||||
}
|
||||
}
|
84
lib/widgets/posts/item.dart
Normal file
84
lib/widgets/posts/item.dart
Normal file
@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/content/article.dart';
|
||||
import 'package:solian/widgets/posts/content/attachment.dart';
|
||||
import 'package:solian/widgets/posts/content/moment.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
class PostItem extends StatefulWidget {
|
||||
final Post item;
|
||||
|
||||
const PostItem({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<PostItem> createState() => _PostItemState();
|
||||
}
|
||||
|
||||
class _PostItemState extends State<PostItem> {
|
||||
Map<String, dynamic>? reactionList;
|
||||
|
||||
Widget renderContent() {
|
||||
switch (widget.item.modelType) {
|
||||
case "article":
|
||||
return ArticleContent(item: widget.item, brief: true);
|
||||
default:
|
||||
return MomentContent(item: widget.item, brief: true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget renderAttachments() {
|
||||
if(widget.item.attachments != null && widget.item.attachments!.isNotEmpty) {
|
||||
return AttachmentList(items: widget.item.attachments!);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const borderRadius = Radius.circular(16);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
timeago.format(widget.item.createdAt)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
renderContent(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: renderAttachments(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -14,20 +14,8 @@ class LayoutWrapper extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
drawer: const SolianNavigationDrawer(),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
title: Text(cfg.title),
|
||||
elevation: 10.0,
|
||||
expandedHeight: 50,
|
||||
floating: true,
|
||||
snap: true,
|
||||
),
|
||||
];
|
||||
},
|
||||
body: child ?? Container(),
|
||||
),
|
||||
appBar: AppBar(title: Text(cfg.title)),
|
||||
body: child ?? Container(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user