batch download

This commit is contained in:
wgh19
2024-05-15 18:05:45 +08:00
parent 9760397f28
commit 7e71b5b1ce
9 changed files with 258 additions and 42 deletions

View File

@@ -18,6 +18,7 @@ class _Appdata {
"downloadSubPath": r"/${id}-p${index}.${ext}", "downloadSubPath": r"/${id}-p${index}.${ext}",
"tagsWeight": "", "tagsWeight": "",
"useTranslatedNameForDownload": false, "useTranslatedNameForDownload": false,
"maxDownloadParallelism": 3
}; };
bool lock = false; bool lock = false;

View File

@@ -0,0 +1,144 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/md.dart';
import 'package:pixes/components/message.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/network/download.dart';
import 'package:pixes/utils/translation.dart';
import '../network/network.dart';
class BatchDownloadButton extends StatelessWidget {
const BatchDownloadButton({super.key, required this.request});
final Future<Res<List<Illust>>> Function() request;
@override
Widget build(BuildContext context) {
return Button(
child: const Icon(MdIcons.download, size: 20,),
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
useRootNavigator: false,
builder: (context) => _DownloadDialog(request));
},
);
}
}
class _DownloadDialog extends StatefulWidget {
const _DownloadDialog(this.request);
final Future<Res<List<Illust>>> Function() request;
@override
State<_DownloadDialog> createState() => _DownloadDialogState();
}
class _DownloadDialogState extends State<_DownloadDialog> {
int maxCount = 30;
bool loading = false;
bool cancel = false;
@override
Widget build(BuildContext context) {
return ContentDialog(
title: Text("Batch download".tl),
content: SizedBox(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${"Maximum number of downloads".tl}:'),
const SizedBox(height: 16,),
SizedBox(
height: 42,
width: 196,
child: NumberBox(
value: maxCount,
onChanged: (value) {
if(!loading) {
setState(() => maxCount = value ?? maxCount);
}
},
allowExpressions: true,
mode: SpinButtonPlacementMode.inline,
smallChange: 10,
largeChange: 30,
clearButton: false,
),
)
],
).paddingVertical(8),
),
actions: [
Button(child: Text("Cancel".tl), onPressed: () {
cancel = true;
context.pop();
}),
if(!loading)
FilledButton(onPressed: load, child: Text("Continue".tl))
else
FilledButton(onPressed: (){}, child: const SizedBox(
height: 20,
width: 64,
child: Center(
child: SizedBox.square(
dimension: 18,
child: ProgressRing(
strokeWidth: 1.6,
),
),
),
))
],
);
}
void load() async{
setState(() {
loading = true;
});
var request = widget.request();
List<Illust> all = [];
String? nextUrl;
int retryCount = 0;
while(nextUrl != "end" && all.length < maxCount) {
if(nextUrl != null) {
request = Network().getIllustsWithNextUrl(nextUrl);
}
var res = await request;
if(cancel || !mounted) {
return;
}
if(res.error) {
retryCount++;
if(retryCount > 3) {
setState(() {
loading = false;
});
showToast(context, message: "Error".tl);
return;
}
await Future.delayed(Duration(seconds: 1 << retryCount));
continue;
}
all.addAll(res.data);
nextUrl = res.subData ?? "end";
}
int i = 0;
for(var illust in all) {
if(i > maxCount) return;
DownloadManager().addDownloadingTask(illust);
i++;
}
context.pop();
}
}

View File

@@ -23,7 +23,7 @@ class SegmentedButton<T> extends StatelessWidget {
child: Card( child: Card(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: SizedBox( child: SizedBox(
height: 36, height: 28,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: options.map((e) => buildButton(e)).toList(), children: options.map((e) => buildButton(e)).toList(),

View File

@@ -101,16 +101,29 @@ class DownloadingTask {
} }
imagePaths.add(path); imagePaths.add(path);
_downloadingIndex++; _downloadingIndex++;
_retryCount = 0;
} }
onCompleted?.call(this); onCompleted?.call(this);
} }
catch(e, s) { catch(e, s) {
error = e.toString(); _handleError(e);
_stop = true;
Log.error("Download", "Download error: $e\n$s"); Log.error("Download", "Download error: $e\n$s");
} }
} }
int _retryCount = 0;
void _handleError(Object error) async{
_retryCount++;
if(_retryCount > 3) {
_stop = true;
error = error.toString();
return;
}
await Future.delayed(Duration(seconds: 1 << _retryCount));
_download();
}
static String _generateFilePath(Illust illust, int index, String ext) { static String _generateFilePath(Illust illust, int index, String ext) {
final String downloadPath = appdata.settings["downloadPath"]; final String downloadPath = appdata.settings["downloadPath"];
String subPathPatten = appdata.settings["downloadSubPath"]; String subPathPatten = appdata.settings["downloadSubPath"];
@@ -297,4 +310,32 @@ class DownloadManager {
''', [illustId]); ''', [illustId]);
return res.map((e) => e["path"] as String).toList(); return res.map((e) => e["path"] as String).toList();
} }
Future<void> batchDownload(Future<Res<List<Illust>>> request, int maxCount) async{
List<Illust> all = [];
String? nextUrl;
int retryCount = 0;
while(nextUrl != "end" && all.length < maxCount) {
if(nextUrl != null) {
request = Network().getIllustsWithNextUrl(nextUrl);
}
var res = await request;
if(res.error) {
retryCount++;
if(retryCount > 3) {
throw res.error;
}
await Future.delayed(Duration(seconds: 1 << retryCount));
continue;
}
all.addAll(res.data);
nextUrl = res.subData ?? "end";
}
int i = 0;
for(var illust in all) {
if(i > maxCount) return;
addDownloadingTask(illust);
i++;
}
}
} }

View File

@@ -1,5 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/components/batch_download.dart';
import 'package:pixes/components/segmented_button.dart'; import 'package:pixes/components/segmented_button.dart';
import 'package:pixes/components/title_bar.dart'; import 'package:pixes/components/title_bar.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
@@ -34,7 +35,11 @@ class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
Widget buildTab() { Widget buildTab() {
return TitleBar( return TitleBar(
title: "Bookmarks".tl, title: "Bookmarks".tl,
action: SegmentedButton( action: Row(
children: [
BatchDownloadButton(request: () => Network().getBookmarkedIllusts(restrict)),
const SizedBox(width: 8,),
SegmentedButton(
options: [ options: [
SegmentedButtonOption("public", "Public".tl), SegmentedButtonOption("public", "Public".tl),
SegmentedButtonOption("private", "Private".tl), SegmentedButtonOption("private", "Private".tl),
@@ -47,6 +52,8 @@ class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
} }
}, },
value: restrict, value: restrict,
)
],
), ),
); );
} }

View File

@@ -66,6 +66,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
controller[task.illust.id.toString()] ??= FlyoutController(); controller[task.illust.id.toString()] ??= FlyoutController();
return Card( return Card(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SizedBox( child: SizedBox(
height: 96, height: 96,

View File

@@ -4,6 +4,7 @@ import 'package:pixes/components/title_bar.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
import 'package:pixes/utils/translation.dart'; import 'package:pixes/utils/translation.dart';
import '../components/batch_download.dart';
import '../components/illust_widget.dart'; import '../components/illust_widget.dart';
import '../components/loading.dart'; import '../components/loading.dart';
import '../components/segmented_button.dart'; import '../components/segmented_button.dart';
@@ -34,7 +35,11 @@ class _FollowingArtworksPageState extends State<FollowingArtworksPage> {
Widget buildTab() { Widget buildTab() {
return TitleBar( return TitleBar(
title: "Following".tl, title: "Following".tl,
action: SegmentedButton( action: Row(
children: [
BatchDownloadButton(request: () => Network().getFollowingArtworks(restrict)),
const SizedBox(width: 8,),
SegmentedButton(
options: [ options: [
SegmentedButtonOption("all", "All".tl), SegmentedButtonOption("all", "All".tl),
SegmentedButtonOption("public", "Public".tl), SegmentedButtonOption("public", "Public".tl),
@@ -48,6 +53,8 @@ class _FollowingArtworksPageState extends State<FollowingArtworksPage> {
} }
}, },
value: restrict, value: restrict,
)
],
), ),
); );
} }

View File

@@ -3,8 +3,10 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
import 'package:pixes/utils/translation.dart'; import 'package:pixes/utils/translation.dart';
import '../components/batch_download.dart';
import '../components/illust_widget.dart'; import '../components/illust_widget.dart';
import '../components/loading.dart'; import '../components/loading.dart';
import '../components/title_bar.dart';
import '../network/network.dart'; import '../network/network.dart';
class RankingPage extends StatefulWidget { class RankingPage extends StatefulWidget {
@@ -48,12 +50,12 @@ class _RankingPageState extends State<RankingPage> {
} }
Widget buildHeader() { Widget buildHeader() {
return SizedBox( return TitleBar(
child: Row( title: "Ranking".tl,
action: Row(
children: [ children: [
Text("Ranking".tl, style: const TextStyle( BatchDownloadButton(request: () => Network().getRanking(type)),
fontSize: 20, fontWeight: FontWeight.bold),), const SizedBox(width: 8,),
const Spacer(),
DropDownButton( DropDownButton(
title: Text(types[type]!), title: Text(types[type]!),
items: types.entries.map((e) => MenuFlyoutItem( items: types.entries.map((e) => MenuFlyoutItem(
@@ -66,8 +68,8 @@ class _RankingPageState extends State<RankingPage> {
)).toList(), )).toList(),
) )
], ],
) ),
).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 16)); );
} }
} }

View File

@@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/appdata.dart'; import 'package:pixes/appdata.dart';
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/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
@@ -32,7 +33,12 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
slivers: [ slivers: [
buildUser(), buildUser(),
buildInformation(), buildInformation(),
SliverToBoxAdapter(child: buildHeader("Artworks"),), SliverToBoxAdapter(
child: buildHeader(
"Artworks",
action: BatchDownloadButton(
request: () => Network().getUserIllusts(widget.id))
),),
_UserArtworks(data.id.toString(), key: ValueKey(data.id),), _UserArtworks(data.id.toString(), key: ValueKey(data.id),),
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
], ],
@@ -146,13 +152,21 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
); );
} }
Widget buildHeader(String title) { Widget buildHeader(String title, {Widget? action}) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Text( height: 38,
child: Row(
children: [
Text(
title, title,
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
).toAlign(Alignment.centerLeft)).paddingLeft(16).paddingVertical(4); ).toAlign(Alignment.centerLeft),
const Spacer(),
if(action != null)
action.toAlign(Alignment.centerRight)
],
).paddingHorizontal(16)).paddingTop(8);
} }
Widget buildInformation() { Widget buildInformation() {
@@ -181,7 +195,6 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data!.region), buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data!.region),
buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data!.job), buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data!.job),
buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data!.gender), buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data!.gender),
const SizedBox(height: 8,),
buildHeader("Social Network".tl), buildHeader("Social Network".tl),
buildItem(title: "Webpage", buildItem(title: "Webpage",
content: data!.webpage, content: data!.webpage,