diff --git a/assets/tr.json b/assets/tr.json index 08ee0bf..d3cb6b9 100644 --- a/assets/tr.json +++ b/assets/tr.json @@ -125,7 +125,10 @@ "Pause": "暂停", "Resume": "继续", "Paused": "已暂停", - "Delete all": "删除全部" + "Delete all": "删除全部", + "Related": "相关", + "Related artworks": "相关作品", + "Related users": "相关用户" }, "zh_TW": { "Search": "搜索", @@ -253,6 +256,9 @@ "Pause": "暫停", "Resume": "繼續", "Paused": "已暫停", - "Delete all": "刪除全部" + "Delete all": "刪除全部", + "Related": "相關", + "Related artworks": "相關作品", + "Related users": "相關用戶" } } \ No newline at end of file diff --git a/lib/components/loading.dart b/lib/components/loading.dart index 9a0e9a7..9a8b512 100644 --- a/lib/components/loading.dart +++ b/lib/components/loading.dart @@ -13,6 +13,34 @@ abstract class LoadingState extends 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() { return Center( child: Column( @@ -21,25 +49,7 @@ abstract class LoadingState extends Text(error!), const SizedBox(height: 12), Button( - onPressed: () { - setState(() { - isLoading = true; - error = null; - }); - loadData().then((value) { - if(value.success) { - setState(() { - isLoading = false; - data = value.data; - }); - } else { - setState(() { - isLoading = false; - error = value.errorMessage!; - }); - } - }); - }, + onPressed: retry, child: const Text("Retry"), ) ], @@ -69,15 +79,17 @@ abstract class LoadingState extends @override Widget build(BuildContext context) { + Widget child; + if(isLoading){ - return const Center( - child: ProgressRing(), - ); + child = buildLoading(); } else if (error != null){ - return buildError(); + child = buildError(); } else { - return buildContent(context, data!); + child = buildContent(context, data!); } + + return buildFrame(context, child) ?? child; } } @@ -94,6 +106,8 @@ abstract class MultiPageLoadingState Future>> loadData(int page); + Widget? buildFrame(BuildContext context, Widget child) => null; + Widget buildContent(BuildContext context, final List data); bool get isLoading => _isLoading || _isFirstLoading; @@ -181,12 +195,16 @@ abstract class MultiPageLoadingState @override Widget build(BuildContext context) { + Widget child; + if(_isFirstLoading){ - return buildLoading(context); + child = buildLoading(context); } else if (_error != null){ - return buildError(context, _error!); + child = buildError(context, _error!); } else { - return buildContent(context, _data!); + child = buildContent(context, _data!); } + + return buildFrame(context, child) ?? child; } } diff --git a/lib/components/user_preview.dart b/lib/components/user_preview.dart index d923471..bbc2303 100644 --- a/lib/components/user_preview.dart +++ b/lib/components/user_preview.dart @@ -65,8 +65,9 @@ class _UserPreviewWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const Spacer(), + Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 12,), Row( children: [ Button( @@ -97,9 +98,10 @@ class _UserPreviewWidgetState extends State { child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),), ), ], - ) + ), + const Spacer(), ], - ).paddingVertical(8), + ), ) ], ), diff --git a/lib/network/network.dart b/lib/network/network.dart index 82b5586..8c2689b 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -467,4 +467,24 @@ class Network { return Res.fromErrorRes(res); } } + + Future>> 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>> 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); + } + } } diff --git a/lib/pages/downloaded_page.dart b/lib/pages/downloaded_page.dart index 6b428e3..63aa36c 100644 --- a/lib/pages/downloaded_page.dart +++ b/lib/pages/downloaded_page.dart @@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.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/md.dart'; import 'package:pixes/components/message.dart'; @@ -73,9 +74,11 @@ class _DownloadedPageState extends State { color: ColorScheme.of(context).secondaryContainer ), clipBehavior: Clip.antiAlias, - child: image == null ? null : Image( + child: image == null ? null : AnimatedImage( image: FileImage(image), fit: BoxFit.cover, + width: 96, + height: double.infinity, filterQuality: FilterQuality.medium, ), ), diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index 8795b28..65304c7 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -4,10 +4,12 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show Icons; 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/loading.dart'; import 'package:pixes/components/message.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/image_provider.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:share_plus/share_plus.dart'; +import '../components/illust_widget.dart'; import '../components/md.dart'; import '../components/ugoira.dart'; const _kBottomBarHeight = 64.0; +class IllustGalleryPage extends StatefulWidget { + const IllustGalleryPage({super.key}); + + @override + State createState() => _IllustGalleryPageState(); +} + +class _IllustGalleryPageState extends State { + @override + Widget build(BuildContext context) { + // TODO + return const Placeholder(); + } +} + + class IllustPage extends StatefulWidget { 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( onPressed: () { 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, child: Row( children: [ - Icon( - Icons.share, - color: ColorScheme.of(context).error, - size: 18, - ), + const Icon(Icons.share, size: 18,), const SizedBox(width: 8,), Text("Share".tl) ], ), ), ), - const SizedBox(width: 8,), + const SizedBox(width: 6,), Button( onPressed: () { var text = "https://pixiv.net/artworks/${widget.illust.id}"; @@ -740,19 +755,31 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ height: 28, child: Row( children: [ - Icon( - Icons.copy, - color: ColorScheme.of(context).error, - size: 18, - ), + const Icon(Icons.copy, size: 18), const SizedBox(width: 8,), 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 { 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 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>> 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; + } +} + diff --git a/lib/pages/user_info_page.dart b/lib/pages/user_info_page.dart index 0543b58..3455adf 100644 --- a/lib/pages/user_info_page.dart +++ b/lib/pages/user_info_page.dart @@ -6,6 +6,7 @@ import 'package:pixes/components/batch_download.dart'; import 'package:pixes/components/loading.dart'; import 'package:pixes/components/md.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/image_provider.dart'; import 'package:pixes/network/network.dart'; @@ -35,6 +36,10 @@ class _UserInfoPageState extends LoadingState { content: CustomScrollView( slivers: [ buildUser(), + SliverToBoxAdapter( + child: buildHeader("Related users".tl), + ), + _RelatedUsers(widget.id), buildInformation(), buildArtworkHeader(), _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> { + @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 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>> loadData() { + return Network().relatedUsers(widget.uid); + } +} +