related users and related artworks

This commit is contained in:
wgh19
2024-05-17 17:16:21 +08:00
parent a9bddd7def
commit 67ebe4e50b
7 changed files with 233 additions and 46 deletions

View File

@@ -125,7 +125,10 @@
"Pause": "暂停", "Pause": "暂停",
"Resume": "继续", "Resume": "继续",
"Paused": "已暂停", "Paused": "已暂停",
"Delete all": "删除全部" "Delete all": "删除全部",
"Related": "相关",
"Related artworks": "相关作品",
"Related users": "相关用户"
}, },
"zh_TW": { "zh_TW": {
"Search": "搜索", "Search": "搜索",
@@ -253,6 +256,9 @@
"Pause": "暫停", "Pause": "暫停",
"Resume": "繼續", "Resume": "繼續",
"Paused": "已暫停", "Paused": "已暫停",
"Delete all": "刪除全部" "Delete all": "刪除全部",
"Related": "相關",
"Related artworks": "相關作品",
"Related users": "相關用戶"
} }
} }

View File

@@ -13,6 +13,34 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
Widget buildContent(BuildContext context, S data); Widget buildContent(BuildContext context, S data);
Widget? buildFrame(BuildContext context, Widget child) => null;
Widget buildLoading() {
return const Center(
child: ProgressRing(),
);
}
void retry() {
setState(() {
isLoading = true;
error = null;
});
loadData().then((value) {
if(value.success) {
setState(() {
isLoading = false;
data = value.data;
});
} else {
setState(() {
isLoading = false;
error = value.errorMessage!;
});
}
});
}
Widget buildError() { Widget buildError() {
return Center( return Center(
child: Column( child: Column(
@@ -21,25 +49,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
Text(error!), Text(error!),
const SizedBox(height: 12), const SizedBox(height: 12),
Button( Button(
onPressed: () { onPressed: retry,
setState(() {
isLoading = true;
error = null;
});
loadData().then((value) {
if(value.success) {
setState(() {
isLoading = false;
data = value.data;
});
} else {
setState(() {
isLoading = false;
error = value.errorMessage!;
});
}
});
},
child: const Text("Retry"), child: const Text("Retry"),
) )
], ],
@@ -69,15 +79,17 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object> extends
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child;
if(isLoading){ if(isLoading){
return const Center( child = buildLoading();
child: ProgressRing(),
);
} else if (error != null){ } else if (error != null){
return buildError(); child = buildError();
} else { } else {
return buildContent(context, data!); child = buildContent(context, data!);
} }
return buildFrame(context, child) ?? child;
} }
} }
@@ -94,6 +106,8 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
Future<Res<List<S>>> loadData(int page); Future<Res<List<S>>> loadData(int page);
Widget? buildFrame(BuildContext context, Widget child) => null;
Widget buildContent(BuildContext context, final List<S> data); Widget buildContent(BuildContext context, final List<S> data);
bool get isLoading => _isLoading || _isFirstLoading; bool get isLoading => _isLoading || _isFirstLoading;
@@ -181,12 +195,16 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child;
if(_isFirstLoading){ if(_isFirstLoading){
return buildLoading(context); child = buildLoading(context);
} else if (_error != null){ } else if (_error != null){
return buildError(context, _error!); child = buildError(context, _error!);
} else { } else {
return buildContent(context, _data!); child = buildContent(context, _data!);
} }
return buildFrame(context, child) ?? child;
} }
} }

View File

@@ -65,8 +65,9 @@ class _UserPreviewWidgetState extends State<UserPreviewWidget> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const Spacer(), const Spacer(),
Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12,),
Row( Row(
children: [ children: [
Button( Button(
@@ -97,9 +98,10 @@ class _UserPreviewWidgetState extends State<UserPreviewWidget> {
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),), child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
), ),
], ],
) ),
const Spacer(),
], ],
).paddingVertical(8), ),
) )
], ],
), ),

View File

@@ -467,4 +467,24 @@ class Network {
return Res.fromErrorRes(res); return Res.fromErrorRes(res);
} }
} }
Future<Res<List<UserPreview>>> relatedUsers(String id) async {
var res = await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id");
if (res.success) {
return Res(
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList());
} else {
return Res.error(res.errorMessage);
}
}
Future<Res<List<Illust>>> relatedIllusts(String id) async {
var res = await apiGet("/v2/illust/related?filter=for_android&illust_id=$id");
if (res.success) {
return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList());
} else {
return Res.error(res.errorMessage);
}
}
} }

View File

@@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:pixes/components/animated_image.dart';
import 'package:pixes/components/grid.dart'; import 'package:pixes/components/grid.dart';
import 'package:pixes/components/md.dart'; import 'package:pixes/components/md.dart';
import 'package:pixes/components/message.dart'; import 'package:pixes/components/message.dart';
@@ -73,9 +74,11 @@ class _DownloadedPageState extends State<DownloadedPage> {
color: ColorScheme.of(context).secondaryContainer color: ColorScheme.of(context).secondaryContainer
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: image == null ? null : Image( child: image == null ? null : AnimatedImage(
image: FileImage(image), image: FileImage(image),
fit: BoxFit.cover, fit: BoxFit.cover,
width: 96,
height: double.infinity,
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
), ),
), ),

View File

@@ -4,10 +4,12 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show Icons; import 'package:flutter/material.dart' show Icons;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/animated_image.dart';
import 'package:pixes/components/loading.dart'; import 'package:pixes/components/loading.dart';
import 'package:pixes/components/message.dart'; import 'package:pixes/components/message.dart';
import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/page_route.dart';
import 'package:pixes/components/title_bar.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';
@@ -18,12 +20,29 @@ import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/translation.dart'; import 'package:pixes/utils/translation.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../components/illust_widget.dart';
import '../components/md.dart'; import '../components/md.dart';
import '../components/ugoira.dart'; import '../components/ugoira.dart';
const _kBottomBarHeight = 64.0; const _kBottomBarHeight = 64.0;
class IllustGalleryPage extends StatefulWidget {
const IllustGalleryPage({super.key});
@override
State<IllustGalleryPage> createState() => _IllustGalleryPageState();
}
class _IllustGalleryPageState extends State<IllustGalleryPage> {
@override
Widget build(BuildContext context) {
// TODO
return const Placeholder();
}
}
class IllustPage extends StatefulWidget { class IllustPage extends StatefulWidget {
const IllustPage(this.illust, {this.favoriteCallback, super.key}); const IllustPage(this.illust, {this.favoriteCallback, super.key});
@@ -709,7 +728,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{
), ),
), ),
), ),
const SizedBox(width: 8,), const SizedBox(width: 6,),
Button( Button(
onPressed: () { onPressed: () {
Share.share("${widget.illust.title}\nhttps://pixiv.net/artworks/${widget.illust.id}"); Share.share("${widget.illust.title}\nhttps://pixiv.net/artworks/${widget.illust.id}");
@@ -718,18 +737,14 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{
height: 28, height: 28,
child: Row( child: Row(
children: [ children: [
Icon( const Icon(Icons.share, size: 18,),
Icons.share,
color: ColorScheme.of(context).error,
size: 18,
),
const SizedBox(width: 8,), const SizedBox(width: 8,),
Text("Share".tl) Text("Share".tl)
], ],
), ),
), ),
), ),
const SizedBox(width: 8,), const SizedBox(width: 6,),
Button( Button(
onPressed: () { onPressed: () {
var text = "https://pixiv.net/artworks/${widget.illust.id}"; var text = "https://pixiv.net/artworks/${widget.illust.id}";
@@ -740,19 +755,31 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{
height: 28, height: 28,
child: Row( child: Row(
children: [ children: [
Icon( const Icon(Icons.copy, size: 18),
Icons.copy,
color: ColorScheme.of(context).error,
size: 18,
),
const SizedBox(width: 8,), const SizedBox(width: 8,),
Text("Link".tl) Text("Link".tl)
], ],
), ),
), ),
), ),
const SizedBox(width: 6,),
Button(
onPressed: () {
context.to(() => _RelatedIllustsPage(widget.illust.id.toString()));
},
child: SizedBox(
height: 28,
child: Row(
children: [
const Icon(Icons.stars, size: 18),
const SizedBox(width: 8,),
Text("Related".tl)
],
),
),
),
], ],
).paddingHorizontal(4).paddingBottom(4); ).paddingHorizontal(2).paddingBottom(4);
} }
} }
@@ -935,3 +962,64 @@ class _IllustPageWithIdState extends LoadingState<IllustPageWithId, Illust> {
return Network().getIllustByID(widget.id); return Network().getIllustByID(widget.id);
} }
} }
class _RelatedIllustsPage extends StatefulWidget {
const _RelatedIllustsPage(this.id, {super.key});
final String id;
@override
State<_RelatedIllustsPage> createState() => _RelatedIllustsPageState();
}
class _RelatedIllustsPageState extends MultiPageLoadingState<_RelatedIllustsPage, Illust> {
@override
Widget? buildFrame(BuildContext context, Widget child) {
return Column(
children: [
TitleBar(title: "Related artworks".tl),
Expanded(
child: child,
)
],
);
}
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
return LayoutBuilder(builder: (context, constrains){
return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8)
+ EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if(index == data.length - 1){
nextPage();
}
return IllustWidget(data[index]);
},
);
});
}
String? nextUrl;
@override
Future<Res<List<Illust>>> loadData(page) async{
if(nextUrl == "end") {
return Res.error("No more data");
}
var res = nextUrl == null
? await Network().relatedIllusts(widget.id)
: await Network().getIllustsWithNextUrl(nextUrl!);
if(!res.error) {
nextUrl = res.subData;
nextUrl ??= "end";
}
return res;
}
}

View File

@@ -6,6 +6,7 @@ import 'package:pixes/components/batch_download.dart';
import 'package:pixes/components/loading.dart'; import 'package:pixes/components/loading.dart';
import 'package:pixes/components/md.dart'; import 'package:pixes/components/md.dart';
import 'package:pixes/components/segmented_button.dart'; import 'package:pixes/components/segmented_button.dart';
import 'package:pixes/components/user_preview.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/network.dart'; import 'package:pixes/network/network.dart';
@@ -35,6 +36,10 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
content: CustomScrollView( content: CustomScrollView(
slivers: [ slivers: [
buildUser(), buildUser(),
SliverToBoxAdapter(
child: buildHeader("Related users".tl),
),
_RelatedUsers(widget.id),
buildInformation(), buildInformation(),
buildArtworkHeader(), buildArtworkHeader(),
_UserArtworks(data.id.toString(), page, key: ValueKey(data.id + page),), _UserArtworks(data.id.toString(), page, key: ValueKey(data.id + page),),
@@ -333,3 +338,48 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
} }
} }
class _RelatedUsers extends StatefulWidget {
const _RelatedUsers(this.uid);
final String uid;
@override
State<_RelatedUsers> createState() => _RelatedUsersState();
}
class _RelatedUsersState extends LoadingState<_RelatedUsers, List<UserPreview>> {
@override
Widget buildFrame(BuildContext context, Widget child) {
return SliverToBoxAdapter(
child: SizedBox(
height: 108,
width: double.infinity,
child: child,
),
);
}
final ScrollController _controller = ScrollController();
@override
Widget buildContent(BuildContext context, List<UserPreview> data) {
return Scrollbar(
controller: _controller,
child: ListView.builder(
controller: _controller,
padding: const EdgeInsets.only(bottom: 8, left: 8),
primary: false,
scrollDirection: Axis.horizontal,
itemCount: data.length,
itemBuilder: (context, index) {
return UserPreviewWidget(data[index]).fixWidth(264);
},
));
}
@override
Future<Res<List<UserPreview>>> loadData() {
return Network().relatedUsers(widget.uid);
}
}