From 6d1fe886b3f999a75920988b2ec2e74991955d8a Mon Sep 17 00:00:00 2001 From: John Wesley Date: Sat, 6 Jan 2024 12:28:02 -0500 Subject: [PATCH] Refactor entry/post items/comments into a single widget --- lib/src/api/oauth.dart | 2 +- lib/src/screens/entries/entry_comment.dart | 212 ++++------ lib/src/screens/entries/entry_item.dart | 268 +++--------- lib/src/screens/posts/post_comment.dart | 212 ++++------ lib/src/screens/posts/post_item.dart | 211 +++------- lib/src/widgets/action_bar.dart | 223 ---------- lib/src/widgets/content_item.dart | 464 +++++++++++++++++++++ lib/src/widgets/redirect_listen.dart | 4 +- 8 files changed, 743 insertions(+), 853 deletions(-) delete mode 100644 lib/src/widgets/action_bar.dart create mode 100644 lib/src/widgets/content_item.dart diff --git a/lib/src/api/oauth.dart b/lib/src/api/oauth.dart index 1b8ae15..2ff40cd 100644 --- a/lib/src/api/oauth.dart +++ b/lib/src/api/oauth.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:interstellar/src/utils/utils.dart'; +import 'package:interstellar/src/widgets/redirect_listen.dart'; const oauthName = 'Interstellar'; const oauthContact = 'contact@kbin.earth'; -const redirectUri = 'http://localhost:46837'; const oauthGrants = ['authorization_code', 'refresh_token']; const oauthScopes = [ 'read', diff --git a/lib/src/screens/entries/entry_comment.dart b/lib/src/screens/entries/entry_comment.dart index bc8a1e5..5d5797e 100644 --- a/lib/src/screens/entries/entry_comment.dart +++ b/lib/src/screens/entries/entry_comment.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/comments.dart' as api_comments; -import 'package:interstellar/src/screens/explore/user_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; -import 'package:interstellar/src/widgets/action_bar.dart'; -import 'package:interstellar/src/widgets/display_name.dart'; -import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/content_item.dart'; import 'package:provider/provider.dart'; class EntryComment extends StatefulWidget { @@ -19,132 +16,92 @@ class EntryComment extends StatefulWidget { } class _EntryCommentState extends State { - bool _isCollapsed = false; - @override Widget build(BuildContext context) { return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 0, 1), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - DisplayName( - widget.comment.user.username, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => UserScreen( - widget.comment.user.userId, - ), - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - timeDiffFormat(widget.comment.createdAt), - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ) - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Markdown(widget.comment.body), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ActionBar( - boosts: widget.comment.uv, - upVotes: widget.comment.favourites, - downVotes: widget.comment.dv, - isBoosted: widget.comment.userVote == 1, - isUpVoted: widget.comment.isFavourited == true, - isDownVoted: widget.comment.userVote == -1, - isCollapsed: _isCollapsed, - onBoost: whenLoggedIn(context, () async { - var newValue = await api_comments.putVote( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - 1, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onUpVote: whenLoggedIn(context, () async { - var newValue = await api_comments.putFavorite( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onDownVote: whenLoggedIn(context, () async { - var newValue = await api_comments.putVote( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - -1, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onCollapse: widget.comment.childCount > 0 - ? () => setState(() { - _isCollapsed = !_isCollapsed; - }) - : null, - onReply: (body) async { - var newSubComment = await api_comments.postComment( - context.read().httpClient, - context.read().instanceHost, - body, - widget.comment.entryId, - parentCommentId: widget.comment.commentId, - ); + child: ContentItem( + body: widget.comment.body, + createdAt: widget.comment.createdAt, + user: widget.comment.user.username, + userIcon: widget.comment.user.avatar?.storageUrl, + userIdOnClick: widget.comment.user.userId, + boosts: widget.comment.uv, + isBoosted: widget.comment.userVote == 1, + onBoost: whenLoggedIn(context, () async { + var newValue = await api_comments.putVote( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + 1, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + upVotes: widget.comment.favourites, + isUpVoted: widget.comment.isFavourited == true, + onUpVote: whenLoggedIn(context, () async { + var newValue = await api_comments.putFavorite( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + downVotes: widget.comment.dv, + isDownVoted: widget.comment.userVote == -1, + onDownVote: whenLoggedIn(context, () async { + var newValue = await api_comments.putVote( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + -1, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + showCollapse: true, + onReply: (body) async { + var newSubComment = await api_comments.postComment( + context.read().httpClient, + context.read().instanceHost, + body, + widget.comment.entryId, + parentCommentId: widget.comment.commentId, + ); - var newComment = widget.comment; - newComment.childCount += 1; - newComment.children!.insert(0, newSubComment); - widget.onUpdate(newComment); - }, - onEdit: whenLoggedIn(context, (body) async { - var newComment = await api_comments.editComment( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - body, - widget.comment.lang, - widget.comment.isAdult - ); - setState(() { - widget.comment.body = newComment.body; - }); - }), - onDelete: whenLoggedIn(context, () async { - await api_comments.deleteComment( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - ); - setState(() { - widget.comment.body = "deleted"; - }); - }), - ), - ), - const SizedBox(height: 4), - if (!_isCollapsed && widget.comment.childCount > 0) - Column( + var newComment = widget.comment; + newComment.childCount += 1; + newComment.children!.insert(0, newSubComment); + widget.onUpdate(newComment); + }, + onEdit: whenLoggedIn(context, (body) async { + var newComment = await api_comments.editComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + body, + widget.comment.lang, + widget.comment.isAdult); + setState(() { + widget.comment.body = newComment.body; + }); + }, matchesUsername: widget.comment.user.username), + onDelete: whenLoggedIn(context, () async { + await api_comments.deleteComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + setState(() { + widget.comment.body = "deleted"; + }); + }, matchesUsername: widget.comment.user.username), + child: widget.comment.childCount > 0 + ? Column( children: widget.comment.children! .asMap() .entries @@ -155,8 +112,7 @@ class _EntryCommentState extends State { })) .toList(), ) - ], - ), + : null, ), ); } diff --git a/lib/src/screens/entries/entry_item.dart b/lib/src/screens/entries/entry_item.dart index 905de22..1090c77 100644 --- a/lib/src/screens/entries/entry_item.dart +++ b/lib/src/screens/entries/entry_item.dart @@ -1,14 +1,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/entries.dart' as api_entries; -import 'package:interstellar/src/screens/explore/domain_screen.dart'; -import 'package:interstellar/src/screens/explore/magazine_screen.dart'; -import 'package:interstellar/src/screens/explore/user_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; -import 'package:interstellar/src/widgets/action_bar.dart'; -import 'package:interstellar/src/widgets/display_name.dart'; -import 'package:interstellar/src/widgets/markdown.dart'; -import 'package:interstellar/src/widgets/open_webpage.dart'; +import 'package:interstellar/src/widgets/content_item.dart'; import 'package:interstellar/src/widgets/video.dart'; import 'package:provider/provider.dart'; @@ -30,212 +24,68 @@ class EntryItem extends StatelessWidget { final Future Function()? onDelete; final bool isPreview; - _onImageClick(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - title: Text(item.title), - backgroundColor: const Color(0x66000000), - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: InteractiveViewer( - child: Image.network( - item.image!.storageUrl, - ), - ), - ) - ], - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { final isVideo = item.url != null && isSupportedVideo(item.url!); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isPreview && isVideo) VideoPlayer(Uri.parse(item.url!)), - if (item.image?.storageUrl != null && !(!isPreview && isVideo)) - isPreview - ? (isVideo - ? Image.network( - item.image!.storageUrl, - height: 160, - width: double.infinity, - fit: BoxFit.cover, - ) - : InkWell( - onTap: () => _onImageClick(context), - child: Image.network( - item.image!.storageUrl, - height: 160, - width: double.infinity, - fit: BoxFit.cover, - ), - )) - : Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 2, - ), - child: InkWell( - onTap: () => _onImageClick(context), - child: Image.network( - item.image!.storageUrl, - ), - )), - Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - item.url != null - ? InkWell( - child: Text( - item.title, - style: Theme.of(context) - .textTheme - .titleLarge! - .apply(decoration: TextDecoration.underline), - ), - onTap: () { - openWebpage(context, Uri.parse(item.url!)); - }, - ) - : Text( - item.title, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 10), - Row( - children: [ - DisplayName( - item.magazine.name, - icon: item.magazine.icon?.storageUrl, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => MagazineScreen( - item.magazine.magazineId, - ), - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - timeDiffFormat(item.createdAt), - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ), - DisplayName( - item.user.username, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => UserScreen(item.user.userId), - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: IconButton( - tooltip: item.domain.name, - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => DomainScreen( - item.domain.domainId, - data: item.domain, - ), - ), - ); - }, - icon: const Icon(Icons.public), - iconSize: 16, - style: const ButtonStyle( - minimumSize: - MaterialStatePropertyAll(Size.fromRadius(16))), - ), - ), - ], - ), - if (item.body != null && item.body!.isNotEmpty) - const SizedBox(height: 10), - if (item.body != null && item.body!.isNotEmpty) - isPreview - ? Text( - item.body!, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ) - : Markdown(item.body!), - const SizedBox(height: 10), - ActionBar( - boosts: item.uv, - upVotes: item.favourites, - downVotes: item.dv, - isBoosted: item.userVote == 1, - isUpVoted: item.isFavourited == true, - isDownVoted: item.userVote == -1, - onBoost: whenLoggedIn(context, () async { - onUpdate(await api_entries.putVote( - context.read().httpClient, - context.read().instanceHost, - item.entryId, - 1, - )); - }), - onUpVote: whenLoggedIn(context, () async { - onUpdate(await api_entries.putFavorite( - context.read().httpClient, - context.read().instanceHost, - item.entryId, - )); - }), - onDownVote: whenLoggedIn(context, () async { - onUpdate(await api_entries.putVote( - context.read().httpClient, - context.read().instanceHost, - item.entryId, - -1, - )); - }), - onReply: onReply, - onEdit: whenLoggedIn( - context, - onEdit, - matchesUsername: item.user.username, - ), - onDelete: whenLoggedIn( - context, - onDelete, - matchesUsername: item.user.username, - ), - initEdit: () { - return item.body; - }, - leadingWidgets: [ - const Icon(Icons.comment), - const SizedBox(width: 4), - Text(intFormat(item.numComments)), - const SizedBox(width: 8), - ], - ), - ], - ), - ), - ], + return ContentItem( + title: item.title, + image: item.image?.storageUrl, + link: item.url != null ? Uri.parse(item.url!) : null, + video: isVideo ? Uri.parse(item.url!) : null, + body: item.body, + createdAt: item.createdAt, + isPreview: isPreview, + showMagazineFirst: true, + user: item.user.username, + userIcon: item.user.avatar?.storageUrl, + userIdOnClick: item.user.userId, + magazine: item.magazine.name, + magazineIcon: item.magazine.icon?.storageUrl, + magazineIdOnClick: item.magazine.magazineId, + domain: item.domain.name, + domainIdOnClick: item.domain.domainId, + boosts: item.uv, + isBoosted: item.userVote == 1, + onBoost: whenLoggedIn(context, () async { + onUpdate(await api_entries.putVote( + context.read().httpClient, + context.read().instanceHost, + item.entryId, + 1, + )); + }), + upVotes: item.favourites, + isUpVoted: item.isFavourited == true, + onUpVote: whenLoggedIn(context, () async { + onUpdate(await api_entries.putFavorite( + context.read().httpClient, + context.read().instanceHost, + item.entryId, + )); + }), + downVotes: item.dv, + isDownVoted: item.userVote == -1, + onDownVote: whenLoggedIn(context, () async { + onUpdate(await api_entries.putVote( + context.read().httpClient, + context.read().instanceHost, + item.entryId, + -1, + )); + }), + onReply: onReply, + onEdit: whenLoggedIn( + context, + onEdit, + matchesUsername: item.user.username, + ), + onDelete: whenLoggedIn( + context, + onDelete, + matchesUsername: item.user.username, + ), + numComments: item.numComments, ); } } diff --git a/lib/src/screens/posts/post_comment.dart b/lib/src/screens/posts/post_comment.dart index 7d1d492..4140e73 100644 --- a/lib/src/screens/posts/post_comment.dart +++ b/lib/src/screens/posts/post_comment.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/post_comments.dart' as api_comments; -import 'package:interstellar/src/screens/explore/user_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; -import 'package:interstellar/src/widgets/action_bar.dart'; -import 'package:interstellar/src/widgets/display_name.dart'; -import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/content_item.dart'; import 'package:provider/provider.dart'; class PostComment extends StatefulWidget { @@ -19,132 +16,92 @@ class PostComment extends StatefulWidget { } class _EntryCommentState extends State { - bool _isCollapsed = false; - @override Widget build(BuildContext context) { return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 0, 1), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - DisplayName( - widget.comment.user.username, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => UserScreen( - widget.comment.user.userId, - ), - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - timeDiffFormat(widget.comment.createdAt), - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ) - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Markdown(widget.comment.body), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ActionBar( - boosts: widget.comment.uv, - upVotes: widget.comment.favourites, - downVotes: widget.comment.dv, - isBoosted: widget.comment.userVote == 1, - isUpVoted: widget.comment.isFavourited == true, - isDownVoted: widget.comment.userVote == -1, - isCollapsed: _isCollapsed, - onBoost: whenLoggedIn(context, () async { - var newValue = await api_comments.putVote( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - 1, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onUpVote: whenLoggedIn(context, () async { - var newValue = await api_comments.putFavorite( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onDownVote: whenLoggedIn(context, () async { - var newValue = await api_comments.putVote( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - -1, - ); - newValue.childCount = widget.comment.childCount; - newValue.children = widget.comment.children; - widget.onUpdate(newValue); - }), - onCollapse: widget.comment.childCount > 0 - ? () => setState(() { - _isCollapsed = !_isCollapsed; - }) - : null, - onReply: (body) async { - var newSubComment = await api_comments.postComment( - context.read().httpClient, - context.read().instanceHost, - body, - widget.comment.postId, - parentCommentId: widget.comment.commentId, - ); + child: ContentItem( + body: widget.comment.body, + createdAt: widget.comment.createdAt, + user: widget.comment.user.username, + userIcon: widget.comment.user.avatar?.storageUrl, + userIdOnClick: widget.comment.user.userId, + boosts: widget.comment.uv, + isBoosted: widget.comment.userVote == 1, + onBoost: whenLoggedIn(context, () async { + var newValue = await api_comments.putVote( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + 1, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + upVotes: widget.comment.favourites, + onUpVote: whenLoggedIn(context, () async { + var newValue = await api_comments.putFavorite( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + isUpVoted: widget.comment.isFavourited == true, + downVotes: widget.comment.dv, + isDownVoted: widget.comment.userVote == -1, + onDownVote: whenLoggedIn(context, () async { + var newValue = await api_comments.putVote( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + -1, + ); + newValue.childCount = widget.comment.childCount; + newValue.children = widget.comment.children; + widget.onUpdate(newValue); + }), + showCollapse: true, + onReply: (body) async { + var newSubComment = await api_comments.postComment( + context.read().httpClient, + context.read().instanceHost, + body, + widget.comment.postId, + parentCommentId: widget.comment.commentId, + ); - var newComment = widget.comment; - newComment.childCount += 1; - newComment.children!.insert(0, newSubComment); - widget.onUpdate(newComment); - }, - onEdit: whenLoggedIn(context, (body) async { - var newComment = await api_comments.editComment( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - body, - widget.comment.lang, - widget.comment.isAdult - ); - setState(() { - widget.comment.body = newComment.body; - }); - }), - onDelete: whenLoggedIn(context, () async { - await api_comments.deleteComment( - context.read().httpClient, - context.read().instanceHost, - widget.comment.commentId, - ); - setState(() { - widget.comment.body = "deleted"; - }); - }), - ), - ), - const SizedBox(height: 4), - if (!_isCollapsed && widget.comment.childCount > 0) - Column( + var newComment = widget.comment; + newComment.childCount += 1; + newComment.children!.insert(0, newSubComment); + widget.onUpdate(newComment); + }, + onEdit: whenLoggedIn(context, (body) async { + var newComment = await api_comments.editComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + body, + widget.comment.lang, + widget.comment.isAdult); + setState(() { + widget.comment.body = newComment.body; + }); + }), + onDelete: whenLoggedIn(context, () async { + await api_comments.deleteComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + setState(() { + widget.comment.body = "deleted"; + }); + }), + child: widget.comment.childCount > 0 + ? Column( children: widget.comment.children! .asMap() .entries @@ -155,8 +112,7 @@ class _EntryCommentState extends State { })) .toList(), ) - ], - ), + : null, ), ); } diff --git a/lib/src/screens/posts/post_item.dart b/lib/src/screens/posts/post_item.dart index 9d11dfe..a0fe3ff 100644 --- a/lib/src/screens/posts/post_item.dart +++ b/lib/src/screens/posts/post_item.dart @@ -1,12 +1,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/posts.dart' as api_posts; -import 'package:interstellar/src/screens/explore/magazine_screen.dart'; -import 'package:interstellar/src/screens/explore/user_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; -import 'package:interstellar/src/widgets/action_bar.dart'; -import 'package:interstellar/src/widgets/display_name.dart'; -import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/content_item.dart'; import 'package:provider/provider.dart'; class PostItem extends StatelessWidget { @@ -27,165 +23,56 @@ class PostItem extends StatelessWidget { final Future Function()? onDelete; final bool isPreview; - _onImageClick(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - title: Text(item.user.username), - backgroundColor: const Color(0x66000000), - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: InteractiveViewer( - child: Image.network( - item.image!.storageUrl, - ), - ), - ) - ], - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (item.image?.storageUrl != null) - isPreview - ? (InkWell( - onTap: () => _onImageClick(context), - child: Image.network( - item.image!.storageUrl, - height: 160, - width: double.infinity, - fit: BoxFit.cover, - ), - )) - : Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 2, - ), - child: InkWell( - onTap: () => _onImageClick(context), - child: Image.network( - item.image!.storageUrl, - ), - )), - Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - Row( - children: [ - DisplayName( - item.user.username, - icon: item.user.avatar?.storageUrl, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => UserScreen(item.user.userId), - ), - ); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - timeDiffFormat(item.createdAt), - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ), - DisplayName( - item.magazine.name, - icon: item.magazine.icon?.storageUrl, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => MagazineScreen( - item.magazine.magazineId, - ), - ), - ); - }, - ), - ], - ), - if (item.body != null && item.body!.isNotEmpty) - const SizedBox(height: 10), - if (item.body != null && item.body!.isNotEmpty) - isPreview - ? Text( - item.body!, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ) - : Markdown(item.body!), - const SizedBox(height: 10), - ActionBar( - boosts: item.uv, - upVotes: item.favourites, - downVotes: item.dv, - isBoosted: item.userVote == 1, - isUpVoted: item.isFavourited == true, - isDownVoted: item.userVote == -1, - onBoost: whenLoggedIn(context, () async { - onUpdate(await api_posts.putVote( - context.read().httpClient, - context.read().instanceHost, - item.postId, - 1, - )); - }), - onUpVote: whenLoggedIn(context, () async { - onUpdate(await api_posts.putFavorite( - context.read().httpClient, - context.read().instanceHost, - item.postId, - )); - }), - onDownVote: whenLoggedIn(context, () async { - onUpdate(await api_posts.putVote( - context.read().httpClient, - context.read().instanceHost, - item.postId, - -1, - )); - }), - onReply: onReply, - onEdit: whenLoggedIn( - context, - onEdit, - matchesUsername: item.user.username, - ), - onDelete: whenLoggedIn( - context, - onDelete, - matchesUsername: item.user.username, - ), - initEdit: () { - return item.body; - }, - leadingWidgets: [ - const Icon(Icons.comment), - const SizedBox(width: 4), - Text(intFormat(item.numComments)), - const SizedBox(width: 8), - ], - ), - ], - ), - ), - ], + return ContentItem( + body: item.body, + image: item.image?.storageUrl, + createdAt: item.createdAt, + user: item.user.username, + userIcon: item.user.avatar?.storageUrl, + userIdOnClick: item.user.userId, + boosts: item.uv, + isBoosted: item.userVote == 1, + onBoost: whenLoggedIn(context, () async { + onUpdate(await api_posts.putVote( + context.read().httpClient, + context.read().instanceHost, + item.postId, + 1, + )); + }), + upVotes: item.favourites, + isUpVoted: item.isFavourited == true, + onUpVote: whenLoggedIn(context, () async { + onUpdate(await api_posts.putFavorite( + context.read().httpClient, + context.read().instanceHost, + item.postId, + )); + }), + downVotes: item.dv, + isDownVoted: item.userVote == -1, + onDownVote: whenLoggedIn(context, () async { + onUpdate(await api_posts.putVote( + context.read().httpClient, + context.read().instanceHost, + item.postId, + -1, + )); + }), + onReply: onReply, + onEdit: whenLoggedIn( + context, + onEdit, + matchesUsername: item.user.username, + ), + onDelete: whenLoggedIn( + context, + onDelete, + matchesUsername: item.user.username, + ), + numComments: item.numComments, ); } } diff --git a/lib/src/widgets/action_bar.dart b/lib/src/widgets/action_bar.dart deleted file mode 100644 index 70fcd9d..0000000 --- a/lib/src/widgets/action_bar.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:interstellar/src/utils/utils.dart'; -import 'package:interstellar/src/widgets/markdown_editor.dart'; - -class ActionBar extends StatefulWidget { - final int? boosts; - final int? upVotes; - final int? downVotes; - - final bool isBoosted; - final bool isUpVoted; - final bool isDownVoted; - final bool isCollapsed; - - final void Function()? onBoost; - final void Function()? onUpVote; - final void Function()? onDownVote; - final void Function()? onCollapse; - final Future Function(String)? onReply; - final Future Function(String)? onEdit; - final void Function()? onDelete; - final String? Function()? initEdit; - - final List? leadingWidgets; - - const ActionBar({ - super.key, - this.boosts, - this.upVotes, - this.downVotes, - this.isBoosted = false, - this.isUpVoted = false, - this.isDownVoted = false, - this.isCollapsed = false, - this.onBoost, - this.onUpVote, - this.onDownVote, - this.onReply, - this.onCollapse, - this.onEdit, - this.onDelete, - this.initEdit, - this.leadingWidgets, - }); - - @override - State createState() => _ActionBarState(); -} - -class _ActionBarState extends State { - TextEditingController? _replyTextController; - TextEditingController? _editTextController; - final MenuController _menuController = MenuController(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - ...(widget.leadingWidgets ?? []), - if (widget.onReply != null) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - icon: const Icon(Icons.reply), - onPressed: () => setState(() { - _replyTextController = TextEditingController(); - }), - ), - ), - if (widget.onCollapse != null) - IconButton( - tooltip: widget.isCollapsed ? 'Expand' : 'Collapse', - onPressed: widget.onCollapse, - icon: widget.isCollapsed - ? const Icon(Icons.expand_more) - : const Icon(Icons.expand_less)), - const Spacer(), - Padding( - padding: const EdgeInsets.only(left: 12), - child: MenuAnchor( - builder: (BuildContext context, MenuController controller, Widget? child) { - return IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () { - if (_menuController.isOpen) { - _menuController.close(); - } else { - _menuController.open(); - } - }, - ); - }, - controller: _menuController, - menuChildren: [ - MenuItemButton( - onPressed: widget.onEdit != null ? () => setState(() { - _editTextController = TextEditingController(); - }) : null, - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Edit") - ), - ), - MenuItemButton( - onPressed: widget.onDelete, - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Delete") - ), - ), - ] - ), - ), - if (widget.boosts != null) - Padding( - padding: const EdgeInsets.only(left: 12), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.rocket_launch), - color: widget.isBoosted ? Colors.purple.shade400 : null, - onPressed: widget.onBoost, - ), - Text(intFormat(widget.boosts!)) - ], - ), - ), - if (widget.upVotes != null || widget.downVotes != null) - Padding( - padding: const EdgeInsets.only(left: 12), - child: Row( - children: [ - if (widget.upVotes != null) - IconButton( - icon: const Icon(Icons.arrow_upward), - color: widget.isUpVoted ? Colors.green.shade400 : null, - onPressed: widget.onUpVote, - ), - Text(intFormat( - (widget.upVotes ?? 0) - (widget.downVotes ?? 0))), - if (widget.downVotes != null) - IconButton( - icon: const Icon(Icons.arrow_downward), - color: widget.isDownVoted ? Colors.red.shade400 : null, - onPressed: widget.onDownVote, - ), - ], - ), - ) - ], - ), - if (widget.onReply != null && _replyTextController != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - MarkdownEditor(_replyTextController!), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () => setState(() { - _replyTextController!.dispose(); - _replyTextController = null; - }), - child: const Text('Cancel')), - const SizedBox(width: 8), - FilledButton( - onPressed: () async { - // Wait in case of errors before closing - await widget.onReply!(_replyTextController!.text); - - setState(() { - _replyTextController!.dispose(); - _replyTextController = null; - }); - }, - child: const Text('Submit')) - ], - ) - ], - ), - ), - if (widget.onEdit != null && _editTextController != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - MarkdownEditor(_editTextController!..text = (widget.initEdit != null ? widget.initEdit!() : "")!), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () => setState(() { - _editTextController!.dispose(); - _editTextController = null; - }), - child: const Text('Cancel')), - const SizedBox(width: 8), - FilledButton( - onPressed: () async { - // Wait in case of errors before closing - await widget.onEdit!(_editTextController!.text); - - setState(() { - _editTextController!.dispose(); - _editTextController = null; - }); - }, - child: const Text('Submit')) - ], - ) - ], - ), - ) - ], - ); - } -} diff --git a/lib/src/widgets/content_item.dart b/lib/src/widgets/content_item.dart new file mode 100644 index 0000000..a26d6a0 --- /dev/null +++ b/lib/src/widgets/content_item.dart @@ -0,0 +1,464 @@ +import 'package:flutter/material.dart'; +import 'package:interstellar/src/screens/explore/domain_screen.dart'; +import 'package:interstellar/src/screens/explore/magazine_screen.dart'; +import 'package:interstellar/src/screens/explore/user_screen.dart'; +import 'package:interstellar/src/utils/utils.dart'; +import 'package:interstellar/src/widgets/display_name.dart'; +import 'package:interstellar/src/widgets/markdown.dart'; +import 'package:interstellar/src/widgets/markdown_editor.dart'; +import 'package:interstellar/src/widgets/open_webpage.dart'; +import 'package:interstellar/src/widgets/video.dart'; + +class ContentItem extends StatefulWidget { + final String? title; + final String? image; + final Uri? link; + final Uri? video; + final String? body; + final DateTime? createdAt; + + final bool isPreview; + final bool showCollapse; + final bool showMagazineFirst; + + final String? user; + final String? userIcon; + final int? userIdOnClick; + + final String? magazine; + final String? magazineIcon; + final int? magazineIdOnClick; + + final String? domain; + final int? domainIdOnClick; + + final int? boosts; + final bool isBoosted; + final void Function()? onBoost; + + final int? upVotes; + final bool isUpVoted; + final void Function()? onUpVote; + + final int? downVotes; + final bool isDownVoted; + final void Function()? onDownVote; + + final int? numComments; + final Future Function(String)? onReply; + final Future Function(String)? onEdit; + final Future Function()? onDelete; + + final Widget? child; + + const ContentItem( + {this.title, + this.image, + this.link, + this.video, + this.body, + this.createdAt, + this.isPreview = false, + this.showCollapse = false, + this.showMagazineFirst = false, + this.user, + this.userIcon, + this.userIdOnClick, + this.magazine, + this.magazineIcon, + this.magazineIdOnClick, + this.domain, + this.domainIdOnClick, + this.boosts, + this.isBoosted = false, + this.onBoost, + this.upVotes, + this.isUpVoted = false, + this.onUpVote, + this.downVotes, + this.isDownVoted = false, + this.onDownVote, + this.numComments, + this.onReply, + this.onEdit, + this.onDelete, + this.child, + super.key}); + + @override + State createState() => _ContentItemState(); +} + +class _ContentItemState extends State { + bool _isCollapsed = false; + TextEditingController? _replyTextController; + TextEditingController? _editTextController; + final MenuController _menuController = MenuController(); + + _onImageClick(BuildContext context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + title: widget.title != null ? Text(widget.title!) : null, + backgroundColor: const Color(0x66000000), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: InteractiveViewer( + child: Image.network(widget.image!), + ), + ) + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final Widget? userWidget = widget.user != null + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: DisplayName( + widget.user!, + icon: widget.userIcon, + onTap: widget.userIdOnClick != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => UserScreen( + widget.userIdOnClick!, + ), + ), + ) + : null, + ), + ) + : null; + final Widget? magazineWidget = widget.magazine != null + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: DisplayName( + widget.magazine!, + icon: widget.magazineIcon, + onTap: widget.magazineIdOnClick != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MagazineScreen( + widget.magazineIdOnClick!, + ), + ), + ) + : null, + ), + ) + : null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!widget.isPreview && widget.video != null) + VideoPlayer(widget.video!), + if (widget.image != null && + !(!widget.isPreview && widget.video != null)) + widget.isPreview + ? (widget.video != null + ? Image.network( + widget.image!, + height: 160, + width: double.infinity, + fit: BoxFit.cover, + ) + : InkWell( + onTap: () => _onImageClick(context), + child: Image.network( + widget.image!, + height: 160, + width: double.infinity, + fit: BoxFit.cover, + ), + )) + : Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height / 2, + ), + child: InkWell( + onTap: () => _onImageClick(context), + child: Image.network(widget.image!), + )), + Container( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title != null) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: widget.link != null + ? InkWell( + child: Text( + widget.title!, + style: Theme.of(context) + .textTheme + .titleLarge! + .apply(decoration: TextDecoration.underline), + ), + onTap: () { + openWebpage(context, widget.link!); + }, + ) + : Text( + widget.title!, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Row( + children: [ + if (!widget.showMagazineFirst && userWidget != null) + userWidget, + if (widget.showMagazineFirst && magazineWidget != null) + magazineWidget, + if (widget.createdAt != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + timeDiffFormat(widget.createdAt!), + style: const TextStyle(fontWeight: FontWeight.w300), + ), + ), + if (widget.showMagazineFirst && userWidget != null) + userWidget, + if (!widget.showMagazineFirst && magazineWidget != null) + magazineWidget, + if (widget.domain != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: IconButton( + tooltip: widget.domain, + onPressed: widget.domainIdOnClick != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DomainScreen( + widget.domainIdOnClick!, + ), + ), + ) + : null, + icon: const Icon(Icons.public), + iconSize: 16, + style: const ButtonStyle( + minimumSize: + MaterialStatePropertyAll(Size.fromRadius(16))), + ), + ), + ], + ), + if (widget.body != null) const SizedBox(height: 10), + if (widget.body != null) + widget.isPreview + ? Text( + widget.body!, + maxLines: 4, + overflow: TextOverflow.ellipsis, + ) + : Markdown(widget.body!), + const SizedBox(height: 10), + Row( + children: [ + if (widget.numComments != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + const Icon(Icons.comment), + const SizedBox(width: 4), + Text(intFormat(widget.numComments!)) + ], + ), + ), + if (widget.onReply != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + icon: const Icon(Icons.reply), + onPressed: () => setState(() { + _replyTextController = TextEditingController(); + }), + ), + ), + if (widget.showCollapse && widget.child != null) + IconButton( + tooltip: _isCollapsed ? 'Expand' : 'Collapse', + onPressed: () => setState(() { + _isCollapsed = !_isCollapsed; + }), + icon: _isCollapsed + ? const Icon(Icons.expand_more) + : const Icon(Icons.expand_less)), + const Spacer(), + Padding( + padding: const EdgeInsets.only(left: 12), + child: MenuAnchor( + builder: (BuildContext context, + MenuController controller, Widget? child) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + if (_menuController.isOpen) { + _menuController.close(); + } else { + _menuController.open(); + } + }, + ); + }, + controller: _menuController, + menuChildren: [ + MenuItemButton( + onPressed: widget.onEdit != null + ? () => setState(() { + _editTextController = + TextEditingController(); + }) + : null, + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Edit")), + ), + MenuItemButton( + onPressed: widget.onDelete, + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Delete")), + ), + ]), + ), + if (widget.boosts != null) + Padding( + padding: const EdgeInsets.only(left: 12), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.rocket_launch), + color: widget.isBoosted + ? Colors.purple.shade400 + : null, + onPressed: widget.onBoost, + ), + Text(intFormat(widget.boosts!)) + ], + ), + ), + if (widget.upVotes != null || widget.downVotes != null) + Padding( + padding: const EdgeInsets.only(left: 12), + child: Row( + children: [ + if (widget.upVotes != null) + IconButton( + icon: const Icon(Icons.arrow_upward), + color: widget.isUpVoted + ? Colors.green.shade400 + : null, + onPressed: widget.onUpVote, + ), + Text(intFormat( + (widget.upVotes ?? 0) - (widget.downVotes ?? 0))), + if (widget.downVotes != null) + IconButton( + icon: const Icon(Icons.arrow_downward), + color: widget.isDownVoted + ? Colors.red.shade400 + : null, + onPressed: widget.onDownVote, + ), + ], + ), + ) + ], + ), + if (widget.onReply != null && _replyTextController != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + MarkdownEditor(_replyTextController!), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () => setState(() { + _replyTextController!.dispose(); + _replyTextController = null; + }), + child: const Text('Cancel')), + const SizedBox(width: 8), + FilledButton( + onPressed: () async { + // Wait in case of errors before closing + await widget + .onReply!(_replyTextController!.text); + + setState(() { + _replyTextController!.dispose(); + _replyTextController = null; + }); + }, + child: const Text('Submit')) + ], + ) + ], + ), + ), + if (widget.onEdit != null && _editTextController != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + MarkdownEditor( + _editTextController!..text = widget.body ?? '', + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () => setState(() { + _editTextController!.dispose(); + _editTextController = null; + }), + child: const Text('Cancel')), + const SizedBox(width: 8), + FilledButton( + onPressed: () async { + // Wait in case of errors before closing + await widget.onEdit!(_editTextController!.text); + + setState(() { + _editTextController!.dispose(); + _editTextController = null; + }); + }, + child: const Text('Submit')) + ], + ) + ], + ), + ), + if (widget.child != null && !_isCollapsed) + Padding( + padding: const EdgeInsets.only(top: 10), + child: widget.child, + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/widgets/redirect_listen.dart b/lib/src/widgets/redirect_listen.dart index 6996b39..9d32ec0 100644 --- a/lib/src/widgets/redirect_listen.dart +++ b/lib/src/widgets/redirect_listen.dart @@ -7,7 +7,7 @@ import 'package:webview_flutter/webview_flutter.dart'; const _redirectHost = 'localhost'; const _redirectPort = 46837; -const _redirectUri = 'http://$_redirectHost:$_redirectPort'; +const redirectUri = 'http://$_redirectHost:$_redirectPort'; class RedirectListener extends StatefulWidget { final Uri initUri; @@ -52,7 +52,7 @@ class _RedirectListenerState extends State { ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (NavigationRequest request) { - if (request.url.startsWith(_redirectUri)) { + if (request.url.startsWith(redirectUri)) { WebViewCookieManager().clearCookies(); Navigator.pop(context, Uri.parse(request.url).queryParameters); return NavigationDecision.prevent;