diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart deleted file mode 100644 index f0a6bd4..0000000 --- a/lib/widgets/post.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:intl/intl.dart'; -import 'package:lemmy_api_client/v3.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart' as ul; - -import '../hooks/delayed_loading.dart'; -import '../hooks/logged_in_action.dart'; -import '../hooks/stores.dart'; -import '../l10n/l10n.dart'; -import '../pages/create_post.dart'; -import '../pages/full_post.dart'; -import '../stores/accounts_store.dart'; -import '../url_launcher.dart'; -import '../util/cleanup_url.dart'; -import '../util/extensions/api.dart'; -import '../util/extensions/datetime.dart'; -import '../util/goto.dart'; -import '../util/more_icon.dart'; -import '../util/share.dart'; -import 'avatar.dart'; -import 'bottom_modal.dart'; -import 'fullscreenable_image.dart'; -import 'info_table_popup.dart'; -import 'markdown_text.dart'; -import 'save_post_button.dart'; - -enum MediaType { - image, - gallery, - video, - other, - none, -} - -MediaType whatType(String? url) { - if (url == null || url.isEmpty) return MediaType.none; - - // TODO: make detection more nuanced - if (url.endsWith('.jpg') || - url.endsWith('.jpeg') || - url.endsWith('.png') || - url.endsWith('.gif') || - url.endsWith('.webp') || - url.endsWith('.bmp') || - url.endsWith('.wbpm')) { - return MediaType.image; - } - return MediaType.other; -} - -/// A post overview card -class PostWidget extends HookWidget { - final PostView post; - final String instanceHost; - final bool fullPost; - - PostWidget(this.post, {this.fullPost = false}) - : instanceHost = post.instanceHost; - - // == ACTIONS == - - static void showMoreMenu({ - required BuildContext context, - required PostView post, - bool fullPost = false, - }) { - final isMine = context - .read() - .defaultUserDataFor(post.instanceHost) - ?.userId == - post.creator.id; - - showBottomModal( - context: context, - builder: (context) => Column( - children: [ - ListTile( - leading: const Icon(Icons.open_in_browser), - title: const Text('Open in browser'), - onTap: () async => await ul.canLaunch(post.post.apId) - ? ul.launch(post.post.apId) - : ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("can't open in browser"))), - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('Nerd stuff'), - onTap: () { - showInfoTablePopup(context: context, table: { - '% of upvotes': - '${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%', - ...post.toJson(), - }); - }, - ), - if (isMine) - ListTile( - leading: const Icon(Icons.edit), - title: const Text('Edit'), - onTap: () async { - final postView = await showCupertinoModalPopup( - context: context, - builder: (_) => CreatePostPage.edit(post.post), - ); - - if (postView != null) { - Navigator.of(context).pop(); - if (fullPost) { - await goToReplace( - context, - (_) => FullPostPage.fromPostView(postView), - ); - } else { - await goTo( - context, - (_) => FullPostPage.fromPostView(postView), - ); - } - } - }, - ), - ], - ), - ); - } - - // == UI == - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - void _openLink(String url) => - linkLauncher(context: context, url: url, instanceHost: instanceHost); - - final urlDomain = () { - if (whatType(post.post.url) == MediaType.none) return null; - - return urlHost(post.post.url!); - }(); - - /// assemble info section - Widget info() => Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (post.community.icon != null) - Padding( - padding: const EdgeInsets.only(right: 10), - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () => goToCommunity.byId( - context, instanceHost, post.community.id), - child: Avatar( - url: post.community.icon, - noBlank: true, - radius: 20, - ), - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RichText( - overflow: - TextOverflow.ellipsis, // TODO: fix overflowing - text: TextSpan( - style: TextStyle( - fontSize: 15, - color: theme.textTheme.bodyText1?.color), - children: [ - const TextSpan( - text: '!', - style: - TextStyle(fontWeight: FontWeight.w300)), - TextSpan( - text: post.community.name, - style: const TextStyle( - fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => goToCommunity.byId( - context, - instanceHost, - post.community.id)), - const TextSpan( - text: '@', - style: - TextStyle(fontWeight: FontWeight.w300)), - TextSpan( - text: post.post.originInstanceHost, - style: const TextStyle( - fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => goToInstance(context, - post.post.originInstanceHost)), - ], - ), - ) - ], - ), - Row( - children: [ - RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: TextStyle( - fontSize: 13, - color: theme.textTheme.bodyText1?.color), - children: [ - TextSpan( - text: L10n.of(context)!.by, - style: const TextStyle( - fontWeight: FontWeight.w300), - ), - TextSpan( - text: ' ${post.creator.originPreferredName}', - style: const TextStyle( - fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => goToUser.fromPersonSafe( - context, - post.creator, - ), - ), - TextSpan( - text: - ' 路 ${post.post.published.fancyShort}'), - if (post.post.locked) - const TextSpan(text: ' 路 馃敀'), - if (post.post.stickied) - const TextSpan(text: ' 路 馃搶'), - if (post.post.nsfw) const TextSpan(text: ' 路 '), - if (post.post.nsfw) - TextSpan( - text: L10n.of(context)!.nsfw, - style: - const TextStyle(color: Colors.red)), - if (urlDomain != null) - TextSpan(text: ' 路 $urlDomain'), - if (post.post.removed) - const TextSpan(text: ' 路 REMOVED'), - if (post.post.deleted) - const TextSpan(text: ' 路 DELETED'), - ], - ), - ) - ], - ), - ], - ), - const Spacer(), - if (!fullPost) - Column( - children: [ - IconButton( - onPressed: () => - showMoreMenu(context: context, post: post), - icon: Icon(moreIcon), - padding: const EdgeInsets.all(0), - visualDensity: VisualDensity.compact, - ) - ], - ) - ], - ), - ), - ], - ); - - /// assemble title section - Widget title() => Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), - child: Row( - children: [ - Expanded( - flex: 100, - child: Text( - post.post.name, - textAlign: TextAlign.left, - softWrap: true, - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.w600), - ), - ), - if (whatType(post.post.url) == MediaType.other && - post.post.thumbnailUrl != null) ...[ - const Spacer(), - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () => _openLink(post.post.url!), - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: CachedNetworkImage( - imageUrl: post.post.thumbnailUrl!, - width: 70, - height: 70, - fit: BoxFit.cover, - errorWidget: (context, url, error) => - Text(error.toString()), - ), - ), - const Positioned( - top: 8, - right: 8, - child: Icon( - Icons.launch, - size: 20, - ), - ) - ], - ), - ) - ] - ], - ), - ); - - /// assemble link preview - Widget linkPreview() { - assert(post.post.url != null); - - return Padding( - padding: const EdgeInsets.all(10), - child: InkWell( - onTap: () => _openLink(post.post.url!), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).iconTheme.color!.withAlpha(170)), - borderRadius: BorderRadius.circular(5)), - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - Row( - children: [ - const Spacer(), - Text('$urlDomain ', - style: theme.textTheme.caption - ?.apply(fontStyle: FontStyle.italic)), - const Icon(Icons.launch, size: 12), - ], - ), - Row( - children: [ - Flexible( - child: Text( - post.post.embedTitle ?? '', - style: theme.textTheme.subtitle1 - ?.apply(fontWeightDelta: 2), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - if (post.post.embedDescription != null && - post.post.embedDescription!.isNotEmpty) - Row( - children: [ - Flexible( - child: Text( - post.post.embedDescription!, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ], - ), - ), - ), - ), - ); - } - - /// assemble image - Widget postImage() { - assert(post.post.url != null); - - return FullscreenableImage( - url: post.post.url!, - child: CachedNetworkImage( - imageUrl: post.post.url!, - errorWidget: (_, __, ___) => const Icon(Icons.warning), - 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: [ - const Icon(Icons.comment), - const SizedBox(width: 6), - Expanded( - flex: 999, - child: Text( - L10n.of(context)!.number_of_comments(post.counts.comments), - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - const Spacer(), - if (!fullPost) - IconButton( - icon: const Icon(Icons.share), - onPressed: () => share(post.post.apId, context: context), - ), - if (!fullPost) SavePostButton(post), - _Voting(post), - ], - ), - ); - - return Container( - decoration: BoxDecoration( - boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black45)], - color: theme.cardColor, - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: GestureDetector( - onTap: fullPost - ? null - : () => goTo(context, (context) => FullPostPage.fromPostView(post)), - child: Material( - type: MaterialType.transparency, - child: Column( - children: [ - info(), - title(), - if (whatType(post.post.url) != MediaType.other && - whatType(post.post.url) != MediaType.none) - postImage() - else if (post.post.url != null && post.post.url!.isNotEmpty) - linkPreview(), - if (post.post.body != null && fullPost) - Padding( - padding: const EdgeInsets.all(10), - child: MarkdownText( - post.post.body!, - instanceHost: instanceHost, - selectable: true, - ), - ), - if (post.post.body != null && !fullPost) - LayoutBuilder( - builder: (context, constraints) { - final span = TextSpan( - text: post.post.body, - ); - final tp = TextPainter( - text: span, - maxLines: 10, - textDirection: Directionality.of(context), - )..layout(maxWidth: constraints.maxWidth - 20); - - if (tp.didExceedMaxLines) { - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: tp.height), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ClipRect( - child: Align( - alignment: Alignment.topCenter, - heightFactor: 0.8, - child: Padding( - padding: const EdgeInsets.all(10), - child: MarkdownText(post.post.body!, - instanceHost: instanceHost)), - ), - ), - Container( - height: tp.preferredLineHeight * 2.5, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - theme.cardColor.withAlpha(0), - theme.cardColor, - ], - ), - ), - ), - ], - ), - ); - } else { - return Padding( - padding: const EdgeInsets.all(10), - child: MarkdownText(post.post.body!, - instanceHost: instanceHost)); - } - }, - ), - actions(), - ], - ), - ), - ), - ); - } -} - -class _Voting extends HookWidget { - final PostView post; - - final bool wasVoted; - - _Voting(this.post) - : wasVoted = (post.myVote ?? VoteType.none) != VoteType.none; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final myVote = useState(post.myVote ?? VoteType.none); - final loading = useDelayedLoading(); - final showScores = - useConfigStoreSelect((configStore) => configStore.showScores); - final loggedInAction = useLoggedInAction(post.instanceHost); - - vote(VoteType vote, Jwt token) async { - final api = LemmyApiV3(post.instanceHost); - - loading.start(); - try { - final res = await api.run( - CreatePostLike(postId: post.post.id, score: vote, auth: token.raw)); - myVote.value = res.myVote ?? VoteType.none; - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('voting failed :('))); - return; - } - loading.cancel(); - } - - return Row( - children: [ - IconButton( - icon: Icon( - Icons.arrow_upward, - color: myVote.value == VoteType.up ? theme.accentColor : null, - ), - onPressed: loggedInAction( - (token) => vote( - myVote.value == VoteType.up ? VoteType.none : VoteType.up, - token, - ), - ), - ), - if (loading.loading) - const SizedBox( - width: 20, height: 20, child: CircularProgressIndicator()) - else if (showScores) - Text(NumberFormat.compact() - .format(post.counts.score + (wasVoted ? 0 : myVote.value.value))), - IconButton( - icon: Icon( - Icons.arrow_downward, - color: myVote.value == VoteType.down ? Colors.red : null, - ), - onPressed: loggedInAction( - (token) => vote( - myVote.value == VoteType.down ? VoteType.none : VoteType.down, - token, - ), - ), - ), - ], - ); - } -} diff --git a/lib/widgets/save_post_button.dart b/lib/widgets/save_post_button.dart deleted file mode 100644 index a00d48b..0000000 --- a/lib/widgets/save_post_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:lemmy_api_client/v3.dart'; - -import '../hooks/delayed_loading.dart'; -import '../hooks/logged_in_action.dart'; - -// TODO: sync this button between post and fullpost. the same with voting - -class SavePostButton extends HookWidget { - final PostView post; - - const SavePostButton(this.post); - - @override - Widget build(BuildContext context) { - final isSaved = useState(post.saved); - final savedIcon = isSaved.value ? Icons.bookmark : Icons.bookmark_border; - final loading = useDelayedLoading(); - final loggedInAction = useLoggedInAction(post.instanceHost); - - savePost(Jwt token) async { - final api = LemmyApiV3(post.instanceHost); - - loading.start(); - try { - final res = await api.run(SavePost( - postId: post.post.id, save: !isSaved.value, auth: token.raw)); - isSaved.value = res.saved; - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('saving failed :('))); - } - loading.cancel(); - } - - if (loading.loading) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5), - child: SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator( - backgroundColor: Theme.of(context).iconTheme.color, - )), - ); - } - - return IconButton( - tooltip: 'Save post', - icon: Icon(savedIcon), - onPressed: loggedInAction(loading.pending ? (_) {} : savePost), - ); - } -} diff --git a/lib/widgets/sortable_infinite_list.dart b/lib/widgets/sortable_infinite_list.dart index e4fcca2..7c4edeb 100644 --- a/lib/widgets/sortable_infinite_list.dart +++ b/lib/widgets/sortable_infinite_list.dart @@ -7,7 +7,7 @@ import '../hooks/infinite_scroll.dart'; import '../hooks/stores.dart'; import 'comment/comment.dart'; import 'infinite_scroll.dart'; -import 'post.dart'; +import 'post/post.dart'; import 'post_list_options.dart'; typedef FetcherWithSorting = Future> Function( @@ -74,7 +74,7 @@ class InfinitePostList extends SortableInfiniteList { }) : super( itemBuilder: (post) => Column( children: [ - PostWidget(post), + PostTile.fromPostView(post), const SizedBox(height: 20), ], ),