Refactor entry/post items/comments into a single widget

This commit is contained in:
John Wesley 2024-01-06 12:28:02 -05:00
parent f339a9035c
commit 6d1fe886b3
8 changed files with 743 additions and 853 deletions

View File

@ -2,10 +2,10 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/utils/utils.dart';
import 'package:interstellar/src/widgets/redirect_listen.dart';
const oauthName = 'Interstellar'; const oauthName = 'Interstellar';
const oauthContact = 'contact@kbin.earth'; const oauthContact = 'contact@kbin.earth';
const redirectUri = 'http://localhost:46837';
const oauthGrants = ['authorization_code', 'refresh_token']; const oauthGrants = ['authorization_code', 'refresh_token'];
const oauthScopes = [ const oauthScopes = [
'read', 'read',

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:interstellar/src/api/comments.dart' as api_comments; 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/screens/settings/settings_controller.dart';
import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/utils/utils.dart';
import 'package:interstellar/src/widgets/action_bar.dart'; import 'package:interstellar/src/widgets/content_item.dart';
import 'package:interstellar/src/widgets/display_name.dart';
import 'package:interstellar/src/widgets/markdown.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryComment extends StatefulWidget { class EntryComment extends StatefulWidget {
@ -19,132 +16,92 @@ class EntryComment extends StatefulWidget {
} }
class _EntryCommentState extends State<EntryComment> { class _EntryCommentState extends State<EntryComment> {
bool _isCollapsed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
child: Padding( child: ContentItem(
padding: const EdgeInsets.fromLTRB(8, 8, 0, 1), body: widget.comment.body,
child: Column( createdAt: widget.comment.createdAt,
crossAxisAlignment: CrossAxisAlignment.stretch, user: widget.comment.user.username,
children: [ userIcon: widget.comment.user.avatar?.storageUrl,
Row( userIdOnClick: widget.comment.user.userId,
children: [ boosts: widget.comment.uv,
DisplayName( isBoosted: widget.comment.userVote == 1,
widget.comment.user.username, onBoost: whenLoggedIn(context, () async {
onTap: () { var newValue = await api_comments.putVote(
Navigator.of(context).push( context.read<SettingsController>().httpClient,
MaterialPageRoute( context.read<SettingsController>().instanceHost,
builder: (context) => UserScreen( widget.comment.commentId,
widget.comment.user.userId, 1,
), );
), newValue.childCount = widget.comment.childCount;
); newValue.children = widget.comment.children;
}, widget.onUpdate(newValue);
), }),
Padding( upVotes: widget.comment.favourites,
padding: const EdgeInsets.symmetric(horizontal: 10), isUpVoted: widget.comment.isFavourited == true,
child: Text( onUpVote: whenLoggedIn(context, () async {
timeDiffFormat(widget.comment.createdAt), var newValue = await api_comments.putFavorite(
style: const TextStyle(fontWeight: FontWeight.w300), context.read<SettingsController>().httpClient,
), context.read<SettingsController>().instanceHost,
) widget.comment.commentId,
], );
), newValue.childCount = widget.comment.childCount;
Padding( newValue.children = widget.comment.children;
padding: const EdgeInsets.symmetric(vertical: 6), widget.onUpdate(newValue);
child: Markdown(widget.comment.body), }),
), downVotes: widget.comment.dv,
Padding( isDownVoted: widget.comment.userVote == -1,
padding: const EdgeInsets.all(8.0), onDownVote: whenLoggedIn(context, () async {
child: ActionBar( var newValue = await api_comments.putVote(
boosts: widget.comment.uv, context.read<SettingsController>().httpClient,
upVotes: widget.comment.favourites, context.read<SettingsController>().instanceHost,
downVotes: widget.comment.dv, widget.comment.commentId,
isBoosted: widget.comment.userVote == 1, -1,
isUpVoted: widget.comment.isFavourited == true, );
isDownVoted: widget.comment.userVote == -1, newValue.childCount = widget.comment.childCount;
isCollapsed: _isCollapsed, newValue.children = widget.comment.children;
onBoost: whenLoggedIn(context, () async { widget.onUpdate(newValue);
var newValue = await api_comments.putVote( }),
context.read<SettingsController>().httpClient, showCollapse: true,
context.read<SettingsController>().instanceHost, onReply: (body) async {
widget.comment.commentId, var newSubComment = await api_comments.postComment(
1, context.read<SettingsController>().httpClient,
); context.read<SettingsController>().instanceHost,
newValue.childCount = widget.comment.childCount; body,
newValue.children = widget.comment.children; widget.comment.entryId,
widget.onUpdate(newValue); parentCommentId: widget.comment.commentId,
}), );
onUpVote: whenLoggedIn(context, () async {
var newValue = await api_comments.putFavorite(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().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<SettingsController>().httpClient,
context.read<SettingsController>().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<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
body,
widget.comment.entryId,
parentCommentId: widget.comment.commentId,
);
var newComment = widget.comment; var newComment = widget.comment;
newComment.childCount += 1; newComment.childCount += 1;
newComment.children!.insert(0, newSubComment); newComment.children!.insert(0, newSubComment);
widget.onUpdate(newComment); widget.onUpdate(newComment);
}, },
onEdit: whenLoggedIn(context, (body) async { onEdit: whenLoggedIn(context, (body) async {
var newComment = await api_comments.editComment( var newComment = await api_comments.editComment(
context.read<SettingsController>().httpClient, context.read<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost, context.read<SettingsController>().instanceHost,
widget.comment.commentId, widget.comment.commentId,
body, body,
widget.comment.lang, widget.comment.lang,
widget.comment.isAdult widget.comment.isAdult);
); setState(() {
setState(() { widget.comment.body = newComment.body;
widget.comment.body = newComment.body; });
}); }, matchesUsername: widget.comment.user.username),
}), onDelete: whenLoggedIn(context, () async {
onDelete: whenLoggedIn(context, () async { await api_comments.deleteComment(
await api_comments.deleteComment( context.read<SettingsController>().httpClient,
context.read<SettingsController>().httpClient, context.read<SettingsController>().instanceHost,
context.read<SettingsController>().instanceHost, widget.comment.commentId,
widget.comment.commentId, );
); setState(() {
setState(() { widget.comment.body = "deleted";
widget.comment.body = "deleted"; });
}); }, matchesUsername: widget.comment.user.username),
}), child: widget.comment.childCount > 0
), ? Column(
),
const SizedBox(height: 4),
if (!_isCollapsed && widget.comment.childCount > 0)
Column(
children: widget.comment.children! children: widget.comment.children!
.asMap() .asMap()
.entries .entries
@ -155,8 +112,7 @@ class _EntryCommentState extends State<EntryComment> {
})) }))
.toList(), .toList(),
) )
], : null,
),
), ),
); );
} }

View File

@ -1,14 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:interstellar/src/api/entries.dart' as api_entries; 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/screens/settings/settings_controller.dart';
import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/utils/utils.dart';
import 'package:interstellar/src/widgets/action_bar.dart'; import 'package:interstellar/src/widgets/content_item.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/video.dart'; import 'package:interstellar/src/widgets/video.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -30,212 +24,68 @@ class EntryItem extends StatelessWidget {
final Future<void> Function()? onDelete; final Future<void> Function()? onDelete;
final bool isPreview; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isVideo = item.url != null && isSupportedVideo(item.url!); final isVideo = item.url != null && isSupportedVideo(item.url!);
return Column( return ContentItem(
crossAxisAlignment: CrossAxisAlignment.stretch, title: item.title,
children: <Widget>[ image: item.image?.storageUrl,
if (!isPreview && isVideo) VideoPlayer(Uri.parse(item.url!)), link: item.url != null ? Uri.parse(item.url!) : null,
if (item.image?.storageUrl != null && !(!isPreview && isVideo)) video: isVideo ? Uri.parse(item.url!) : null,
isPreview body: item.body,
? (isVideo createdAt: item.createdAt,
? Image.network( isPreview: isPreview,
item.image!.storageUrl, showMagazineFirst: true,
height: 160, user: item.user.username,
width: double.infinity, userIcon: item.user.avatar?.storageUrl,
fit: BoxFit.cover, userIdOnClick: item.user.userId,
) magazine: item.magazine.name,
: InkWell( magazineIcon: item.magazine.icon?.storageUrl,
onTap: () => _onImageClick(context), magazineIdOnClick: item.magazine.magazineId,
child: Image.network( domain: item.domain.name,
item.image!.storageUrl, domainIdOnClick: item.domain.domainId,
height: 160, boosts: item.uv,
width: double.infinity, isBoosted: item.userVote == 1,
fit: BoxFit.cover, onBoost: whenLoggedIn(context, () async {
), onUpdate(await api_entries.putVote(
)) context.read<SettingsController>().httpClient,
: Container( context.read<SettingsController>().instanceHost,
constraints: BoxConstraints( item.entryId,
maxHeight: MediaQuery.of(context).size.height / 2, 1,
), ));
child: InkWell( }),
onTap: () => _onImageClick(context), upVotes: item.favourites,
child: Image.network( isUpVoted: item.isFavourited == true,
item.image!.storageUrl, onUpVote: whenLoggedIn(context, () async {
), onUpdate(await api_entries.putFavorite(
)), context.read<SettingsController>().httpClient,
Container( context.read<SettingsController>().instanceHost,
padding: const EdgeInsets.all(16), item.entryId,
child: Column( ));
crossAxisAlignment: CrossAxisAlignment.start, }),
children: <Widget>[ downVotes: item.dv,
item.url != null isDownVoted: item.userVote == -1,
? InkWell( onDownVote: whenLoggedIn(context, () async {
child: Text( onUpdate(await api_entries.putVote(
item.title, context.read<SettingsController>().httpClient,
style: Theme.of(context) context.read<SettingsController>().instanceHost,
.textTheme item.entryId,
.titleLarge! -1,
.apply(decoration: TextDecoration.underline), ));
), }),
onTap: () { onReply: onReply,
openWebpage(context, Uri.parse(item.url!)); onEdit: whenLoggedIn(
}, context,
) onEdit,
: Text( matchesUsername: item.user.username,
item.title, ),
style: Theme.of(context).textTheme.titleLarge, onDelete: whenLoggedIn(
), context,
const SizedBox(height: 10), onDelete,
Row( matchesUsername: item.user.username,
children: [ ),
DisplayName( numComments: item.numComments,
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<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
item.entryId,
1,
));
}),
onUpVote: whenLoggedIn(context, () async {
onUpdate(await api_entries.putFavorite(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
item.entryId,
));
}),
onDownVote: whenLoggedIn(context, () async {
onUpdate(await api_entries.putVote(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().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),
],
),
],
),
),
],
); );
} }
} }

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:interstellar/src/api/post_comments.dart' as api_comments; 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/screens/settings/settings_controller.dart';
import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/utils/utils.dart';
import 'package:interstellar/src/widgets/action_bar.dart'; import 'package:interstellar/src/widgets/content_item.dart';
import 'package:interstellar/src/widgets/display_name.dart';
import 'package:interstellar/src/widgets/markdown.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PostComment extends StatefulWidget { class PostComment extends StatefulWidget {
@ -19,132 +16,92 @@ class PostComment extends StatefulWidget {
} }
class _EntryCommentState extends State<PostComment> { class _EntryCommentState extends State<PostComment> {
bool _isCollapsed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
child: Padding( child: ContentItem(
padding: const EdgeInsets.fromLTRB(8, 8, 0, 1), body: widget.comment.body,
child: Column( createdAt: widget.comment.createdAt,
crossAxisAlignment: CrossAxisAlignment.stretch, user: widget.comment.user.username,
children: [ userIcon: widget.comment.user.avatar?.storageUrl,
Row( userIdOnClick: widget.comment.user.userId,
children: [ boosts: widget.comment.uv,
DisplayName( isBoosted: widget.comment.userVote == 1,
widget.comment.user.username, onBoost: whenLoggedIn(context, () async {
onTap: () { var newValue = await api_comments.putVote(
Navigator.of(context).push( context.read<SettingsController>().httpClient,
MaterialPageRoute( context.read<SettingsController>().instanceHost,
builder: (context) => UserScreen( widget.comment.commentId,
widget.comment.user.userId, 1,
), );
), newValue.childCount = widget.comment.childCount;
); newValue.children = widget.comment.children;
}, widget.onUpdate(newValue);
), }),
Padding( upVotes: widget.comment.favourites,
padding: const EdgeInsets.symmetric(horizontal: 10), onUpVote: whenLoggedIn(context, () async {
child: Text( var newValue = await api_comments.putFavorite(
timeDiffFormat(widget.comment.createdAt), context.read<SettingsController>().httpClient,
style: const TextStyle(fontWeight: FontWeight.w300), context.read<SettingsController>().instanceHost,
), widget.comment.commentId,
) );
], newValue.childCount = widget.comment.childCount;
), newValue.children = widget.comment.children;
Padding( widget.onUpdate(newValue);
padding: const EdgeInsets.symmetric(vertical: 6), }),
child: Markdown(widget.comment.body), isUpVoted: widget.comment.isFavourited == true,
), downVotes: widget.comment.dv,
Padding( isDownVoted: widget.comment.userVote == -1,
padding: const EdgeInsets.all(8.0), onDownVote: whenLoggedIn(context, () async {
child: ActionBar( var newValue = await api_comments.putVote(
boosts: widget.comment.uv, context.read<SettingsController>().httpClient,
upVotes: widget.comment.favourites, context.read<SettingsController>().instanceHost,
downVotes: widget.comment.dv, widget.comment.commentId,
isBoosted: widget.comment.userVote == 1, -1,
isUpVoted: widget.comment.isFavourited == true, );
isDownVoted: widget.comment.userVote == -1, newValue.childCount = widget.comment.childCount;
isCollapsed: _isCollapsed, newValue.children = widget.comment.children;
onBoost: whenLoggedIn(context, () async { widget.onUpdate(newValue);
var newValue = await api_comments.putVote( }),
context.read<SettingsController>().httpClient, showCollapse: true,
context.read<SettingsController>().instanceHost, onReply: (body) async {
widget.comment.commentId, var newSubComment = await api_comments.postComment(
1, context.read<SettingsController>().httpClient,
); context.read<SettingsController>().instanceHost,
newValue.childCount = widget.comment.childCount; body,
newValue.children = widget.comment.children; widget.comment.postId,
widget.onUpdate(newValue); parentCommentId: widget.comment.commentId,
}), );
onUpVote: whenLoggedIn(context, () async {
var newValue = await api_comments.putFavorite(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().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<SettingsController>().httpClient,
context.read<SettingsController>().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<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
body,
widget.comment.postId,
parentCommentId: widget.comment.commentId,
);
var newComment = widget.comment; var newComment = widget.comment;
newComment.childCount += 1; newComment.childCount += 1;
newComment.children!.insert(0, newSubComment); newComment.children!.insert(0, newSubComment);
widget.onUpdate(newComment); widget.onUpdate(newComment);
}, },
onEdit: whenLoggedIn(context, (body) async { onEdit: whenLoggedIn(context, (body) async {
var newComment = await api_comments.editComment( var newComment = await api_comments.editComment(
context.read<SettingsController>().httpClient, context.read<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost, context.read<SettingsController>().instanceHost,
widget.comment.commentId, widget.comment.commentId,
body, body,
widget.comment.lang, widget.comment.lang,
widget.comment.isAdult widget.comment.isAdult);
); setState(() {
setState(() { widget.comment.body = newComment.body;
widget.comment.body = newComment.body; });
}); }),
}), onDelete: whenLoggedIn(context, () async {
onDelete: whenLoggedIn(context, () async { await api_comments.deleteComment(
await api_comments.deleteComment( context.read<SettingsController>().httpClient,
context.read<SettingsController>().httpClient, context.read<SettingsController>().instanceHost,
context.read<SettingsController>().instanceHost, widget.comment.commentId,
widget.comment.commentId, );
); setState(() {
setState(() { widget.comment.body = "deleted";
widget.comment.body = "deleted"; });
}); }),
}), child: widget.comment.childCount > 0
), ? Column(
),
const SizedBox(height: 4),
if (!_isCollapsed && widget.comment.childCount > 0)
Column(
children: widget.comment.children! children: widget.comment.children!
.asMap() .asMap()
.entries .entries
@ -155,8 +112,7 @@ class _EntryCommentState extends State<PostComment> {
})) }))
.toList(), .toList(),
) )
], : null,
),
), ),
); );
} }

View File

@ -1,12 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:interstellar/src/api/posts.dart' as api_posts; 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/screens/settings/settings_controller.dart';
import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/utils/utils.dart';
import 'package:interstellar/src/widgets/action_bar.dart'; import 'package:interstellar/src/widgets/content_item.dart';
import 'package:interstellar/src/widgets/display_name.dart';
import 'package:interstellar/src/widgets/markdown.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PostItem extends StatelessWidget { class PostItem extends StatelessWidget {
@ -27,165 +23,56 @@ class PostItem extends StatelessWidget {
final Future<void> Function()? onDelete; final Future<void> Function()? onDelete;
final bool isPreview; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return ContentItem(
crossAxisAlignment: CrossAxisAlignment.stretch, body: item.body,
children: <Widget>[ image: item.image?.storageUrl,
if (item.image?.storageUrl != null) createdAt: item.createdAt,
isPreview user: item.user.username,
? (InkWell( userIcon: item.user.avatar?.storageUrl,
onTap: () => _onImageClick(context), userIdOnClick: item.user.userId,
child: Image.network( boosts: item.uv,
item.image!.storageUrl, isBoosted: item.userVote == 1,
height: 160, onBoost: whenLoggedIn(context, () async {
width: double.infinity, onUpdate(await api_posts.putVote(
fit: BoxFit.cover, context.read<SettingsController>().httpClient,
), context.read<SettingsController>().instanceHost,
)) item.postId,
: Container( 1,
constraints: BoxConstraints( ));
maxHeight: MediaQuery.of(context).size.height / 2, }),
), upVotes: item.favourites,
child: InkWell( isUpVoted: item.isFavourited == true,
onTap: () => _onImageClick(context), onUpVote: whenLoggedIn(context, () async {
child: Image.network( onUpdate(await api_posts.putFavorite(
item.image!.storageUrl, context.read<SettingsController>().httpClient,
), context.read<SettingsController>().instanceHost,
)), item.postId,
Container( ));
padding: const EdgeInsets.all(16), }),
child: Column( downVotes: item.dv,
crossAxisAlignment: CrossAxisAlignment.start, isDownVoted: item.userVote == -1,
children: <Widget>[ onDownVote: whenLoggedIn(context, () async {
const SizedBox(height: 10), onUpdate(await api_posts.putVote(
Row( context.read<SettingsController>().httpClient,
children: [ context.read<SettingsController>().instanceHost,
DisplayName( item.postId,
item.user.username, -1,
icon: item.user.avatar?.storageUrl, ));
onTap: () { }),
Navigator.of(context).push( onReply: onReply,
MaterialPageRoute( onEdit: whenLoggedIn(
builder: (context) => UserScreen(item.user.userId), context,
), onEdit,
); matchesUsername: item.user.username,
}, ),
), onDelete: whenLoggedIn(
Padding( context,
padding: const EdgeInsets.symmetric(horizontal: 10), onDelete,
child: Text( matchesUsername: item.user.username,
timeDiffFormat(item.createdAt), ),
style: const TextStyle(fontWeight: FontWeight.w300), numComments: item.numComments,
),
),
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<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
item.postId,
1,
));
}),
onUpVote: whenLoggedIn(context, () async {
onUpdate(await api_posts.putFavorite(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().instanceHost,
item.postId,
));
}),
onDownVote: whenLoggedIn(context, () async {
onUpdate(await api_posts.putVote(
context.read<SettingsController>().httpClient,
context.read<SettingsController>().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),
],
),
],
),
),
],
); );
} }
} }

View File

@ -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<void> Function(String)? onReply;
final Future<void> Function(String)? onEdit;
final void Function()? onDelete;
final String? Function()? initEdit;
final List<Widget>? 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<ActionBar> createState() => _ActionBarState();
}
class _ActionBarState extends State<ActionBar> {
TextEditingController? _replyTextController;
TextEditingController? _editTextController;
final MenuController _menuController = MenuController();
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: <Widget>[
...(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'))
],
)
],
),
)
],
);
}
}

View File

@ -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<void> Function(String)? onReply;
final Future<void> Function(String)? onEdit;
final Future<void> 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<ContentItem> createState() => _ContentItemState();
}
class _ContentItemState extends State<ContentItem> {
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: <Widget>[
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: <Widget>[
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: <Widget>[
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,
)
],
),
),
],
);
}
}

View File

@ -7,7 +7,7 @@ import 'package:webview_flutter/webview_flutter.dart';
const _redirectHost = 'localhost'; const _redirectHost = 'localhost';
const _redirectPort = 46837; const _redirectPort = 46837;
const _redirectUri = 'http://$_redirectHost:$_redirectPort'; const redirectUri = 'http://$_redirectHost:$_redirectPort';
class RedirectListener extends StatefulWidget { class RedirectListener extends StatefulWidget {
final Uri initUri; final Uri initUri;
@ -52,7 +52,7 @@ class _RedirectListenerState extends State<RedirectListener> {
..setNavigationDelegate( ..setNavigationDelegate(
NavigationDelegate( NavigationDelegate(
onNavigationRequest: (NavigationRequest request) { onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith(_redirectUri)) { if (request.url.startsWith(redirectUri)) {
WebViewCookieManager().clearCookies(); WebViewCookieManager().clearCookies();
Navigator.pop(context, Uri.parse(request.url).queryParameters); Navigator.pop(context, Uri.parse(request.url).queryParameters);
return NavigationDecision.prevent; return NavigationDecision.prevent;