mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 04:57:23 +00:00
batch download
This commit is contained in:
@@ -18,6 +18,7 @@ class _Appdata {
|
||||
"downloadSubPath": r"/${id}-p${index}.${ext}",
|
||||
"tagsWeight": "",
|
||||
"useTranslatedNameForDownload": false,
|
||||
"maxDownloadParallelism": 3
|
||||
};
|
||||
|
||||
bool lock = false;
|
||||
|
144
lib/components/batch_download.dart
Normal file
144
lib/components/batch_download.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ class SegmentedButton<T> extends StatelessWidget {
|
||||
child: Card(
|
||||
padding: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
height: 28,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: options.map((e) => buildButton(e)).toList(),
|
||||
|
@@ -101,16 +101,29 @@ class DownloadingTask {
|
||||
}
|
||||
imagePaths.add(path);
|
||||
_downloadingIndex++;
|
||||
_retryCount = 0;
|
||||
}
|
||||
onCompleted?.call(this);
|
||||
}
|
||||
catch(e, s) {
|
||||
error = e.toString();
|
||||
_stop = true;
|
||||
_handleError(e);
|
||||
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) {
|
||||
final String downloadPath = appdata.settings["downloadPath"];
|
||||
String subPathPatten = appdata.settings["downloadSubPath"];
|
||||
@@ -297,4 +310,32 @@ class DownloadManager {
|
||||
''', [illustId]);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.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/title_bar.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
@@ -34,7 +35,11 @@ class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
|
||||
Widget buildTab() {
|
||||
return TitleBar(
|
||||
title: "Bookmarks".tl,
|
||||
action: SegmentedButton(
|
||||
action: Row(
|
||||
children: [
|
||||
BatchDownloadButton(request: () => Network().getBookmarkedIllusts(restrict)),
|
||||
const SizedBox(width: 8,),
|
||||
SegmentedButton(
|
||||
options: [
|
||||
SegmentedButtonOption("public", "Public".tl),
|
||||
SegmentedButtonOption("private", "Private".tl),
|
||||
@@ -47,6 +52,8 @@ class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
|
||||
}
|
||||
},
|
||||
value: restrict,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -66,6 +66,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
||||
controller[task.illust.id.toString()] ??= FlyoutController();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SizedBox(
|
||||
height: 96,
|
||||
|
@@ -4,6 +4,7 @@ import 'package:pixes/components/title_bar.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
import 'package:pixes/utils/translation.dart';
|
||||
|
||||
import '../components/batch_download.dart';
|
||||
import '../components/illust_widget.dart';
|
||||
import '../components/loading.dart';
|
||||
import '../components/segmented_button.dart';
|
||||
@@ -34,7 +35,11 @@ class _FollowingArtworksPageState extends State<FollowingArtworksPage> {
|
||||
Widget buildTab() {
|
||||
return TitleBar(
|
||||
title: "Following".tl,
|
||||
action: SegmentedButton(
|
||||
action: Row(
|
||||
children: [
|
||||
BatchDownloadButton(request: () => Network().getFollowingArtworks(restrict)),
|
||||
const SizedBox(width: 8,),
|
||||
SegmentedButton(
|
||||
options: [
|
||||
SegmentedButtonOption("all", "All".tl),
|
||||
SegmentedButtonOption("public", "Public".tl),
|
||||
@@ -48,6 +53,8 @@ class _FollowingArtworksPageState extends State<FollowingArtworksPage> {
|
||||
}
|
||||
},
|
||||
value: restrict,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -3,8 +3,10 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
import 'package:pixes/utils/translation.dart';
|
||||
|
||||
import '../components/batch_download.dart';
|
||||
import '../components/illust_widget.dart';
|
||||
import '../components/loading.dart';
|
||||
import '../components/title_bar.dart';
|
||||
import '../network/network.dart';
|
||||
|
||||
class RankingPage extends StatefulWidget {
|
||||
@@ -48,12 +50,12 @@ class _RankingPageState extends State<RankingPage> {
|
||||
}
|
||||
|
||||
Widget buildHeader() {
|
||||
return SizedBox(
|
||||
child: Row(
|
||||
return TitleBar(
|
||||
title: "Ranking".tl,
|
||||
action: Row(
|
||||
children: [
|
||||
Text("Ranking".tl, style: const TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold),),
|
||||
const Spacer(),
|
||||
BatchDownloadButton(request: () => Network().getRanking(type)),
|
||||
const SizedBox(width: 8,),
|
||||
DropDownButton(
|
||||
title: Text(types[type]!),
|
||||
items: types.entries.map((e) => MenuFlyoutItem(
|
||||
@@ -66,8 +68,8 @@ class _RankingPageState extends State<RankingPage> {
|
||||
)).toList(),
|
||||
)
|
||||
],
|
||||
)
|
||||
).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 16));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:pixes/appdata.dart';
|
||||
import 'package:pixes/components/batch_download.dart';
|
||||
import 'package:pixes/components/loading.dart';
|
||||
import 'package:pixes/components/md.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
@@ -32,7 +33,12 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
||||
slivers: [
|
||||
buildUser(),
|
||||
buildInformation(),
|
||||
SliverToBoxAdapter(child: buildHeader("Artworks"),),
|
||||
SliverToBoxAdapter(
|
||||
child: buildHeader(
|
||||
"Artworks",
|
||||
action: BatchDownloadButton(
|
||||
request: () => Network().getUserIllusts(widget.id))
|
||||
),),
|
||||
_UserArtworks(data.id.toString(), key: ValueKey(data.id),),
|
||||
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(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
height: 38,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
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() {
|
||||
@@ -181,7 +195,6 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
|
||||
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.person_2_outlined, title: "Gender".tl, content: data!.gender),
|
||||
const SizedBox(height: 8,),
|
||||
buildHeader("Social Network".tl),
|
||||
buildItem(title: "Webpage",
|
||||
content: data!.webpage,
|
||||
|
Reference in New Issue
Block a user