This commit is contained in:
wgh19
2024-05-13 22:39:39 +08:00
parent 1fece674d6
commit 2045dd0741
4 changed files with 339 additions and 27 deletions

View File

@@ -2,46 +2,124 @@ 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, width: width,
height: height, height: height,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), child: Stack(
child: Card( children: [
padding: EdgeInsets.zero, Positioned.fill(child: Container(
margin: EdgeInsets.zero, width: width,
child: MouseRegion( height: height,
cursor: SystemMouseCursors.click, padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: GestureDetector( child: Card(
onTap: (){ padding: EdgeInsets.zero,
context.to(() => IllustPage(illust)); margin: EdgeInsets.zero,
}, child: GestureDetector(
child: ClipRRect( onTap: (){
borderRadius: BorderRadius.circular(4.0), context.to(() => IllustPage(widget.illust, favoriteCallback: (v) {
child: AnimatedImage( setState(() {
image: CachedImageProvider(illust.images.first.medium), widget.illust.isBookmarked = v;
fit: BoxFit.cover, });
width: width-16.0, },));
height: height-16.0, },
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: AnimatedImage(
image: CachedImageProvider(widget.illust.images.first.medium),
fit: BoxFit.cover,
width: width-16.0,
height: height-16.0,
),
),
), ),
), ),
), )),
), 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,
),
),
),
);
}
} }

View File

@@ -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'];
}

View File

@@ -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);
}
}
} }

View File

@@ -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;
}
}