Merge pull request #21 from krawieck/post
This commit is contained in:
commit
cf0d1167cc
|
@ -0,0 +1,33 @@
|
|||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
|
||||
class CommentTree {
|
||||
CommentView comment;
|
||||
List<CommentTree> children;
|
||||
|
||||
CommentTree(this.comment, [this.children]) {
|
||||
children ??= [];
|
||||
}
|
||||
|
||||
static List<CommentTree> fromList(List<CommentView> comments) {
|
||||
CommentTree gatherChildren(CommentTree parent) {
|
||||
for (var el in comments) {
|
||||
if (el.parentId == parent.comment.id) {
|
||||
parent.children.add(gatherChildren(CommentTree(el)));
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
var parents = <CommentTree>[];
|
||||
|
||||
// first pass to get all the parents
|
||||
for (var i = 0; i < comments.length; i++) {
|
||||
if (comments[i].parentId == null) {
|
||||
parents.add(CommentTree(comments[i]));
|
||||
}
|
||||
}
|
||||
|
||||
var result = parents.map(gatherChildren).toList();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lemmy_api_client/src/models/post.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
class PostWidget extends StatelessWidget {
|
||||
final PostView post;
|
||||
final String hostUrl;
|
||||
|
||||
/// nullable
|
||||
final String linkPostDomain;
|
||||
|
||||
ThemeData _theme;
|
||||
//https://images-assets.nasa.gov/image/PIA04921/PIA04921~orig.jpg
|
||||
PostWidget(this.post)
|
||||
: hostUrl = post.communityActorId.split('/')[2],
|
||||
linkPostDomain = post.url != null ? post.url.split('/')[2] : null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_theme = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [BoxShadow(blurRadius: 10, color: Colors.black54)],
|
||||
color: _theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
print('GO TO POST');
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
_info(),
|
||||
_title(),
|
||||
_content(),
|
||||
_actions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _info() {
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (post.communityIcon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: InkWell(
|
||||
onTap: () => print('GO TO COMMUNITY'),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: CachedNetworkImage(
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
imageUrl: post.communityIcon,
|
||||
errorWidget: (context, url, error) =>
|
||||
Text(error.toString()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
RichText(
|
||||
overflow: TextOverflow.ellipsis, // @TODO: fix overflowing
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 15, color: _theme.textTheme.bodyText1.color),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '!',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text: post.communityName,
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => print('GO TO COMMUNITY')),
|
||||
TextSpan(
|
||||
text: '@',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text: hostUrl,
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => print('GO TO INSTANCE')),
|
||||
],
|
||||
),
|
||||
)
|
||||
]),
|
||||
Row(children: [
|
||||
RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _theme.textTheme.bodyText1.color),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'by',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text:
|
||||
''' ${post.creatorPreferredUsername ?? post.creatorName}''',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => print('GO TO USER'),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
''' · ${timeago.format(post.published, locale: 'en_short')}'''),
|
||||
if (linkPostDomain != null)
|
||||
TextSpan(text: ' · $linkPostDomain'),
|
||||
if (post.locked) TextSpan(text: ' · 🔒'),
|
||||
],
|
||||
))
|
||||
]),
|
||||
],
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
),
|
||||
Spacer(),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => print('POPUP MENU'),
|
||||
icon: Icon(Icons.more_vert),
|
||||
)
|
||||
],
|
||||
)
|
||||
]),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _title() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${post.name}',
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (post.thumbnailUrl != null)
|
||||
InkWell(
|
||||
onTap: () => print('OPEN LINK'),
|
||||
child: Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: post.thumbnailUrl,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
Text(error.toString()),
|
||||
)),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Icon(
|
||||
Icons.launch,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _content() {
|
||||
if (post.url == null) return Container();
|
||||
// naive implementation for now but will have
|
||||
// to add system for detecting type of media
|
||||
if (post.url.endsWith('.jpg') ||
|
||||
post.url.endsWith('.png') ||
|
||||
post.url.endsWith('.gif')) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: post.url,
|
||||
progressIndicatorBuilder: (context, url, progress) =>
|
||||
CircularProgressIndicator(value: progress.progress),
|
||||
);
|
||||
} else {
|
||||
return _linkPreview();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _linkPreview() {
|
||||
assert(post.url != null);
|
||||
|
||||
var url = post.url.split('/')[2];
|
||||
if (url.startsWith('www.')) {
|
||||
url = url.substring(4);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: InkWell(
|
||||
onTap: () => print('OPEN LINK'),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(width: 1),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
Spacer(),
|
||||
Text('$url ',
|
||||
style: _theme.textTheme.caption
|
||||
.apply(fontStyle: FontStyle.italic)),
|
||||
Icon(Icons.launch, size: 12),
|
||||
]),
|
||||
Row(children: [
|
||||
Flexible(
|
||||
child: Text(post.embedTitle,
|
||||
style: _theme.textTheme.subtitle1
|
||||
.apply(fontWeightDelta: 2)))
|
||||
]),
|
||||
Row(children: [Flexible(child: Text(post.embedDescription))]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actions() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.comment),
|
||||
post.numberOfComments == 1
|
||||
? Text(' 1 comment')
|
||||
: Text(' ${post.numberOfComments} comments'),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
onPressed: () => Share.text('Share post url', post.apId,
|
||||
'text/plain')), // @TODO: find a way to mark it as url
|
||||
IconButton(
|
||||
icon: post.saved == true
|
||||
? Icon(Icons.bookmark)
|
||||
: Icon(Icons.bookmark_border),
|
||||
onPressed: () => print('SAVE')),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_upward), onPressed: () => print('UPVOTE')),
|
||||
Text(post.score.toString()),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_downward),
|
||||
onPressed: () => print('DOWNVOTE')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
Future<void> urlLauncher(String url) async {
|
||||
if (await ul.canLaunch(url)) {
|
||||
await ul.launch(url);
|
||||
} else {
|
||||
throw Exception();
|
||||
// TODO: handle opening links to stuff in app
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../comment_tree.dart';
|
||||
import 'markdown_text.dart';
|
||||
|
||||
class Comment extends StatelessWidget {
|
||||
final int indent;
|
||||
final int postCreatorId;
|
||||
final CommentTree commentTree;
|
||||
Comment(
|
||||
this.commentTree, {
|
||||
this.indent = 0,
|
||||
@required this.postCreatorId,
|
||||
});
|
||||
|
||||
void _goToUser() {
|
||||
print('GO TO USER');
|
||||
}
|
||||
|
||||
bool get isOP => commentTree.comment.creatorId == postCreatorId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var comment = commentTree.comment;
|
||||
|
||||
// decide which username to use
|
||||
var username;
|
||||
if (comment.creatorPreferredUsername != null &&
|
||||
comment.creatorPreferredUsername != '') {
|
||||
username = comment.creatorPreferredUsername;
|
||||
} else {
|
||||
username = '@${comment.creatorName}';
|
||||
}
|
||||
|
||||
var body;
|
||||
if (comment.deleted) {
|
||||
body = Flexible(
|
||||
child: Text(
|
||||
'comment deleted by creator',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else if (comment.removed) {
|
||||
body = Flexible(
|
||||
child: Text(
|
||||
'comment deleted by moderator',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else {
|
||||
body = Flexible(child: MarkdownText(commentTree.comment.content));
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
if (comment.creatorAvatar != null)
|
||||
InkWell(
|
||||
onTap: _goToUser,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: comment.creatorAvatar,
|
||||
height: 20,
|
||||
width: 20,
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
child: Text(username,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
)),
|
||||
onLongPress: _goToUser,
|
||||
),
|
||||
if (isOP) CommentTag('OP', Theme.of(context).accentColor),
|
||||
if (comment.banned) CommentTag('BANNED', Colors.red),
|
||||
if (comment.bannedFromCommunity)
|
||||
CommentTag('BANNED FROM COMMUNITY', Colors.red),
|
||||
Spacer(),
|
||||
Text(comment.score.toString()),
|
||||
]),
|
||||
Row(children: [body]),
|
||||
Row(children: [
|
||||
Spacer(),
|
||||
// actions go here
|
||||
])
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(10),
|
||||
margin: EdgeInsets.only(left: indent > 1 ? (indent - 1) * 5.0 : 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: indent > 0
|
||||
? BorderSide(color: Colors.red, width: 5)
|
||||
: BorderSide.none,
|
||||
top: BorderSide(width: 0.2))),
|
||||
),
|
||||
for (var c in commentTree.children)
|
||||
Comment(
|
||||
c,
|
||||
indent: indent + 1,
|
||||
postCreatorId: postCreatorId,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommentTag extends StatelessWidget {
|
||||
final String text;
|
||||
final Color bgColor;
|
||||
|
||||
const CommentTag(this.text, this.bgColor);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
color: bgColor,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: Theme.of(context).textTheme.bodyText1.fontSize - 5,
|
||||
fontWeight: FontWeight.w800,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
|
||||
import '../comment_tree.dart';
|
||||
import 'comment.dart';
|
||||
|
||||
/// Manages comments section, sorts them
|
||||
class CommentSection extends HookWidget {
|
||||
final List<CommentView> rawComments;
|
||||
final List<CommentTree> comments;
|
||||
final int postCreatorId;
|
||||
|
||||
CommentSection(this.rawComments, {@required this.postCreatorId})
|
||||
: comments = CommentTree.fromList(rawComments),
|
||||
assert(postCreatorId != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(children: [
|
||||
// sorting menu goes here
|
||||
if (comments.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 50),
|
||||
child: Text(
|
||||
'no comments yet',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
for (var com in comments) Comment(com, postCreatorId: postCreatorId),
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
import '../url_launcher.dart';
|
||||
|
||||
class MarkdownText extends StatelessWidget {
|
||||
final String text;
|
||||
MarkdownText(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MarkdownBody(
|
||||
data: text,
|
||||
extensionSet: md.ExtensionSet.gitHubWeb,
|
||||
onTapLink: (href) {
|
||||
urlLauncher(href)
|
||||
.catchError((e) => Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.warning),
|
||||
Text("couldn't open link"),
|
||||
],
|
||||
),
|
||||
)));
|
||||
},
|
||||
imageBuilder: (uri, title, alt) => CachedNetworkImage(
|
||||
imageUrl: uri.toString(),
|
||||
errorWidget: (context, url, error) => Row(
|
||||
children: [
|
||||
Icon(Icons.warning),
|
||||
Text("couldn't load image, ${error.toString()}")
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
import 'markdown_text.dart';
|
||||
|
||||
enum MediaType {
|
||||
image,
|
||||
gallery,
|
||||
video,
|
||||
other,
|
||||
}
|
||||
|
||||
MediaType whatType(String url) {
|
||||
if (url == null) return MediaType.other;
|
||||
|
||||
// TODO: make detection more nuanced
|
||||
if (url.endsWith('.jpg') || url.endsWith('.png') || url.endsWith('.gif')) {
|
||||
return MediaType.image;
|
||||
}
|
||||
return MediaType.other;
|
||||
}
|
||||
|
||||
class Post extends StatelessWidget {
|
||||
final PostView post;
|
||||
final String instanceUrl;
|
||||
|
||||
/// nullable
|
||||
final String postUrlDomain;
|
||||
|
||||
Post(this.post)
|
||||
: instanceUrl = post.communityActorId.split('/')[2],
|
||||
postUrlDomain = post.url != null ? post.url.split('/')[2] : null;
|
||||
|
||||
// == ACTIONS ==
|
||||
|
||||
void _openLink() {
|
||||
print('OPEN LINK');
|
||||
}
|
||||
|
||||
void _goToUser() {
|
||||
print('GO TO USER');
|
||||
}
|
||||
|
||||
void _goToPost(BuildContext context) {
|
||||
print('GO TO POST');
|
||||
}
|
||||
|
||||
void _goToCommunity() {
|
||||
print('GO TO COMMUNITY');
|
||||
}
|
||||
|
||||
void _goToInstance() {
|
||||
print('GO TO INSTANCE');
|
||||
}
|
||||
|
||||
void _openFullImage() {
|
||||
print('OPEN FULL IMAGE');
|
||||
}
|
||||
|
||||
// POST ACTIONS
|
||||
|
||||
void _savePost() {
|
||||
print('SAVE POST');
|
||||
}
|
||||
|
||||
void _upvotePost() {
|
||||
print('UPVOTE POST');
|
||||
}
|
||||
|
||||
void _downvotePost() {
|
||||
print('DOWNVOTE POST');
|
||||
}
|
||||
|
||||
void _showMoreMenu() {
|
||||
print('SHOW MORE MENU');
|
||||
}
|
||||
|
||||
// == UI ==
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// TODO: add NSFW, locked, removed, deleted, stickied
|
||||
/// assemble info section
|
||||
Widget info() => Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (post.communityIcon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: InkWell(
|
||||
onTap: _goToCommunity,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: CachedNetworkImage(
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
imageUrl: post.communityIcon,
|
||||
errorWidget: (context, url, error) =>
|
||||
Text(error.toString()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
RichText(
|
||||
overflow: TextOverflow.ellipsis, // TODO: fix overflowing
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.textTheme.bodyText1.color),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '!',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text: post.communityName,
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = _goToCommunity),
|
||||
TextSpan(
|
||||
text: '@',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text: instanceUrl,
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = _goToInstance),
|
||||
],
|
||||
),
|
||||
)
|
||||
]),
|
||||
Row(children: [
|
||||
RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.textTheme.bodyText1.color),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'by',
|
||||
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||
TextSpan(
|
||||
text:
|
||||
''' ${post.creatorPreferredUsername ?? post.creatorName}''',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = _goToUser,
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
''' · ${timeago.format(post.published, locale: 'en_short')}'''),
|
||||
if (postUrlDomain != null)
|
||||
TextSpan(text: ' · $postUrlDomain'),
|
||||
if (post.locked) TextSpan(text: ' · 🔒'),
|
||||
],
|
||||
))
|
||||
]),
|
||||
],
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
),
|
||||
Spacer(),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _showMoreMenu,
|
||||
icon: Icon(Icons.more_vert),
|
||||
)
|
||||
],
|
||||
)
|
||||
]),
|
||||
),
|
||||
]);
|
||||
|
||||
/// assemble title section
|
||||
Widget title() => Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${post.name}',
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (post.url != null &&
|
||||
whatType(post.url) == MediaType.other &&
|
||||
post.thumbnailUrl != null)
|
||||
Spacer(),
|
||||
if (post.url != null &&
|
||||
whatType(post.url) == MediaType.other &&
|
||||
post.thumbnailUrl != null)
|
||||
InkWell(
|
||||
onTap: _openLink,
|
||||
child: Stack(children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: post.thumbnailUrl,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
Text(error.toString()),
|
||||
)),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Icon(
|
||||
Icons.launch,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// assemble link preview
|
||||
Widget linkPreview() {
|
||||
assert(post.url != null);
|
||||
|
||||
var url = post.url.split('/')[2];
|
||||
if (url.startsWith('www.')) {
|
||||
url = url.substring(4);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: InkWell(
|
||||
onTap: _openLink,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(width: 1),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
Spacer(),
|
||||
Text('$url ',
|
||||
style: theme.textTheme.caption
|
||||
.apply(fontStyle: FontStyle.italic)),
|
||||
Icon(Icons.launch, size: 12),
|
||||
]),
|
||||
Row(children: [
|
||||
Flexible(
|
||||
child: Text(post.embedTitle,
|
||||
style: theme.textTheme.subtitle1
|
||||
.apply(fontWeightDelta: 2)))
|
||||
]),
|
||||
Row(children: [Flexible(child: Text(post.embedDescription))]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// assemble image
|
||||
Widget postImage() {
|
||||
assert(post.url != null);
|
||||
|
||||
return InkWell(
|
||||
onTap: _openFullImage,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: post.url,
|
||||
progressIndicatorBuilder: (context, url, progress) =>
|
||||
CircularProgressIndicator(value: progress.progress),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// assemble actions section
|
||||
Widget actions() => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.comment),
|
||||
Expanded(
|
||||
flex: 999,
|
||||
child: Text(
|
||||
''' ${NumberFormat.compact().format(post.numberOfComments)} comment${post.numberOfComments == 1 ? '' : 's'}''',
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
onPressed: () => Share.text('Share post url', post.apId,
|
||||
'text/plain')), // TODO: find a way to mark it as url
|
||||
IconButton(
|
||||
icon: post.saved == true
|
||||
? Icon(Icons.bookmark)
|
||||
: Icon(Icons.bookmark_border),
|
||||
onPressed: _savePost),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_upward), onPressed: _upvotePost),
|
||||
Text(NumberFormat.compact().format(post.score)),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_downward), onPressed: _downvotePost),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [BoxShadow(blurRadius: 10, color: Colors.black54)],
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _goToPost(context),
|
||||
child: Column(
|
||||
children: [
|
||||
info(),
|
||||
title(),
|
||||
if (whatType(post.url) != MediaType.other)
|
||||
postImage()
|
||||
else if (post.url != null)
|
||||
linkPreview(),
|
||||
if (post.body != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.body)),
|
||||
actions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
63
pubspec.lock
63
pubspec.lock
|
@ -244,11 +244,23 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.2"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -325,7 +337,7 @@ packages:
|
|||
name: lemmy_api_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.2.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -333,6 +345,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.4"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -431,6 +450,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
platform_detect:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform_detect
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -590,6 +616,41 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+7"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -21,11 +21,14 @@ environment:
|
|||
sdk: '>=2.7.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
url_launcher: ^5.5.1
|
||||
markdown: ^2.1.8
|
||||
flutter_markdown: ^0.4.3
|
||||
esys_flutter_share: ^1.0.2
|
||||
flutter_hooks: ^0.13.2
|
||||
cached_network_image: ^2.2.0+1
|
||||
timeago: ^2.0.27
|
||||
lemmy_api_client: ^0.1.3
|
||||
lemmy_api_client: ^0.2.1
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
|
Loading…
Reference in New Issue