mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 21:07:24 +00:00
comments
This commit is contained in:
@@ -2,37 +2,52 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:pixes/components/animated_image.dart';
|
import 'package:pixes/components/animated_image.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/foundation/image_provider.dart';
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
import 'package:pixes/network/models.dart';
|
|
||||||
|
|
||||||
|
import '../network/network.dart';
|
||||||
import '../pages/illust_page.dart';
|
import '../pages/illust_page.dart';
|
||||||
|
import 'md.dart';
|
||||||
|
|
||||||
class IllustWidget extends StatelessWidget {
|
class IllustWidget extends StatefulWidget {
|
||||||
const IllustWidget(this.illust, {super.key});
|
const IllustWidget(this.illust, {super.key});
|
||||||
|
|
||||||
final Illust illust;
|
final Illust illust;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IllustWidget> createState() => _IllustWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IllustWidgetState extends State<IllustWidget> {
|
||||||
|
bool isBookmarking = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(builder: (context, constrains) {
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
final width = constrains.maxWidth;
|
final width = constrains.maxWidth;
|
||||||
final height = illust.height * width / illust.width;
|
final height = widget.illust.height * width / widget.illust.width;
|
||||||
return Container(
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: Container(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: (){
|
onTap: (){
|
||||||
context.to(() => IllustPage(illust));
|
context.to(() => IllustPage(widget.illust, favoriteCallback: (v) {
|
||||||
|
setState(() {
|
||||||
|
widget.illust.isBookmarked = v;
|
||||||
|
});
|
||||||
|
},));
|
||||||
},
|
},
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
child: AnimatedImage(
|
child: AnimatedImage(
|
||||||
image: CachedImageProvider(illust.images.first.medium),
|
image: CachedImageProvider(widget.illust.images.first.medium),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: width-16.0,
|
width: width-16.0,
|
||||||
height: height-16.0,
|
height: height-16.0,
|
||||||
@@ -40,8 +55,71 @@ class IllustWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)),
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: buildButton(),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildButton() {
|
||||||
|
void favorite() async{
|
||||||
|
if(isBookmarking) return;
|
||||||
|
setState(() {
|
||||||
|
isBookmarking = true;
|
||||||
|
});
|
||||||
|
var method = widget.illust.isBookmarked ? "delete" : "add";
|
||||||
|
var res = await Network().addBookmark(widget.illust.id.toString(), method);
|
||||||
|
if(res.error) {
|
||||||
|
if(mounted) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widget.illust.isBookmarked = !widget.illust.isBookmarked;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isBookmarking = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if(isBookmarking) {
|
||||||
|
child = const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: ProgressRing(strokeWidth: 1.6,),
|
||||||
|
);
|
||||||
|
} else if(widget.illust.isBookmarked) {
|
||||||
|
child = Icon(
|
||||||
|
MdIcons.favorite,
|
||||||
|
color: ColorScheme.of(context).error,
|
||||||
|
size: 22,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
child = Icon(
|
||||||
|
MdIcons.favorite,
|
||||||
|
color: ColorScheme.of(context).outline,
|
||||||
|
size: 22,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: favorite,
|
||||||
|
child: Center(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -342,3 +342,44 @@ class UserPreview {
|
|||||||
isFollowed = json['is_followed'],
|
isFollowed = json['is_followed'],
|
||||||
isBlocking = json['is_access_blocking_user'] ?? false;
|
isBlocking = json['is_access_blocking_user'] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"id": 176418447,
|
||||||
|
"comment": "",
|
||||||
|
"date": "2024-05-13T19:28:13+09:00",
|
||||||
|
"user": {
|
||||||
|
"id": 54898889,
|
||||||
|
"name": "Rorigod",
|
||||||
|
"account": "user_gjzr2787",
|
||||||
|
"profile_image_urls": {
|
||||||
|
"medium": "https://i.pximg.net/user-profile/img/2021/09/01/00/46/58/21334581_94fac3456245d2b680ecf1c60aba2c95_170.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has_replies": false,
|
||||||
|
"stamp": {
|
||||||
|
"stamp_id": 407,
|
||||||
|
"stamp_url": "https://s.pximg.net/common/images/stamp/generated-stamps/407_s.jpg?20180605"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
class Comment{
|
||||||
|
final String id;
|
||||||
|
final String comment;
|
||||||
|
final DateTime date;
|
||||||
|
final String uid;
|
||||||
|
final String name;
|
||||||
|
final String avatar;
|
||||||
|
final bool hasReplies;
|
||||||
|
final String? stampUrl;
|
||||||
|
|
||||||
|
Comment.fromJson(Map<String, dynamic> json)
|
||||||
|
: id = json['id'].toString(),
|
||||||
|
comment = json['comment'],
|
||||||
|
date = DateTime.parse(json['date']),
|
||||||
|
uid = json['user']['id'].toString(),
|
||||||
|
name = json['user']['name'],
|
||||||
|
avatar = json['user']['profile_image_urls']['medium'],
|
||||||
|
hasReplies = json['has_replies'],
|
||||||
|
stampUrl = json['stamp']?['stamp_url'];
|
||||||
|
}
|
||||||
|
@@ -370,4 +370,25 @@ class Network {
|
|||||||
return Res.error(res.errorMessage);
|
return Res.error(res.errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Comment>>> getComments(String id, [String? nextUrl]) async {
|
||||||
|
var res = await apiGet(nextUrl ?? "/v3/illust/comments?illust_id=$id");
|
||||||
|
if (res.success) {
|
||||||
|
return Res(
|
||||||
|
(res.data["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
|
subData: res.data["next_url"]);
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Res<bool>> comment(String id, String content) async {
|
||||||
|
var res = await apiPost("/v1/illust/comment/add",
|
||||||
|
data: {"illust_id": id, "comment": content});
|
||||||
|
if (res.success) {
|
||||||
|
return const Res(true);
|
||||||
|
} else {
|
||||||
|
return Res.fromErrorRes(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:pixes/components/animated_image.dart';
|
import 'package:pixes/components/animated_image.dart';
|
||||||
|
import 'package:pixes/components/loading.dart';
|
||||||
|
import 'package:pixes/components/message.dart';
|
||||||
|
import 'package:pixes/components/page_route.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/foundation/image_provider.dart';
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
import 'package:pixes/network/download.dart';
|
import 'package:pixes/network/download.dart';
|
||||||
@@ -15,10 +18,12 @@ import '../components/md.dart';
|
|||||||
const _kBottomBarHeight = 64.0;
|
const _kBottomBarHeight = 64.0;
|
||||||
|
|
||||||
class IllustPage extends StatefulWidget {
|
class IllustPage extends StatefulWidget {
|
||||||
const IllustPage(this.illust, {super.key});
|
const IllustPage(this.illust, {this.favoriteCallback, super.key});
|
||||||
|
|
||||||
final Illust illust;
|
final Illust illust;
|
||||||
|
|
||||||
|
final void Function(bool)? favoriteCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<IllustPage> createState() => _IllustPageState();
|
State<IllustPage> createState() => _IllustPageState();
|
||||||
}
|
}
|
||||||
@@ -41,7 +46,12 @@ class _IllustPageState extends State<IllustPage> {
|
|||||||
top: 0,
|
top: 0,
|
||||||
child: buildBody(constrains.maxWidth, constrains.maxHeight),
|
child: buildBody(constrains.maxWidth, constrains.maxHeight),
|
||||||
),
|
),
|
||||||
_BottomBar(widget.illust, constrains.maxHeight, constrains.maxWidth),
|
_BottomBar(
|
||||||
|
widget.illust,
|
||||||
|
constrains.maxHeight,
|
||||||
|
constrains.maxWidth,
|
||||||
|
favoriteCallback: widget.favoriteCallback,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -105,7 +115,9 @@ class _IllustPageState extends State<IllustPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BottomBar extends StatefulWidget {
|
class _BottomBar extends StatefulWidget {
|
||||||
const _BottomBar(this.illust, this.height, this.width);
|
const _BottomBar(this.illust, this.height, this.width, {this.favoriteCallback});
|
||||||
|
|
||||||
|
final void Function(bool)? favoriteCallback;
|
||||||
|
|
||||||
final Illust illust;
|
final Illust illust;
|
||||||
|
|
||||||
@@ -335,6 +347,7 @@ class _BottomBarState extends State<_BottomBar> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
widget.illust.isBookmarked = !widget.illust.isBookmarked;
|
widget.illust.isBookmarked = !widget.illust.isBookmarked;
|
||||||
|
widget.favoriteCallback?.call(widget.illust.isBookmarked);
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
isBookmarking = false;
|
isBookmarking = false;
|
||||||
@@ -406,7 +419,7 @@ class _BottomBarState extends State<_BottomBar> {
|
|||||||
yield const SizedBox(width: 8,);
|
yield const SizedBox(width: 8,);
|
||||||
|
|
||||||
yield Button(
|
yield Button(
|
||||||
onPressed: favorite,
|
onPressed: () => _CommentsPage.show(context, widget.illust.id.toString()),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 28,
|
height: 28,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -502,3 +515,162 @@ class _BottomBarState extends State<_BottomBar> {
|
|||||||
).paddingVertical(8).paddingHorizontal(2);
|
).paddingVertical(8).paddingHorizontal(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CommentsPage extends StatefulWidget {
|
||||||
|
const _CommentsPage(this.id);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
static void show(BuildContext context, String id) {
|
||||||
|
Navigator.of(context).push(SideBarRoute(_CommentsPage(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CommentsPage> createState() => _CommentsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentsPageState extends MultiPageLoadingState<_CommentsPage, Comment> {
|
||||||
|
bool isCommenting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildContent(BuildContext context, List<Comment> data) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: buildBody(context, data)),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildBottom(context),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody(BuildContext context, List<Comment> data) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: data.length + 2,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if(index == 0) {
|
||||||
|
return Text("Comments".tl, style: const TextStyle(fontSize: 20)).paddingVertical(8).paddingHorizontal(12);
|
||||||
|
} else if(index == data.length + 1) {
|
||||||
|
return const SizedBox(height: 64,);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
var date = data[index].date;
|
||||||
|
var dateText = "${date.year}/${date.month}/${date.day}";
|
||||||
|
return Card(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 38,
|
||||||
|
width: 38,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(38),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: ColorScheme.of(context).secondaryContainer,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => context.to(() => UserInfoPage(data[index].id.toString())),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(data[index].avatar),
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(data[index].name, style: const TextStyle(fontSize: 14),),
|
||||||
|
Text(dateText, style: TextStyle(fontSize: 12, color: ColorScheme.of(context).outline),)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8,),
|
||||||
|
if(data[index].comment.isNotEmpty)
|
||||||
|
Text(data[index].comment, style: const TextStyle(fontSize: 16),),
|
||||||
|
if(data[index].stampUrl != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 64,
|
||||||
|
width: 64,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(data[index].stampUrl!),
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottom(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.96),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: TextBox(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
placeholder: "Comment".tl,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
onSubmitted: (s) {
|
||||||
|
showToast(context, message: "Sending".tl);
|
||||||
|
if(isCommenting) return;
|
||||||
|
setState(() {
|
||||||
|
isCommenting = true;
|
||||||
|
});
|
||||||
|
Network().comment(widget.id, s).then((value) {
|
||||||
|
if(value.error) {
|
||||||
|
context.showToast(message: "Network Error");
|
||||||
|
setState(() {
|
||||||
|
isCommenting = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isCommenting = false;
|
||||||
|
nextUrl = null;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).paddingVertical(8).paddingHorizontal(12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? nextUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Res<List<Comment>>> loadData(int page) async{
|
||||||
|
if(nextUrl == "end") {
|
||||||
|
return Res.error("No more data");
|
||||||
|
}
|
||||||
|
var res = await Network().getComments(widget.id, nextUrl);
|
||||||
|
if(!res.error) {
|
||||||
|
nextUrl = res.subData;
|
||||||
|
nextUrl ??= "end";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user