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}",
|
"downloadSubPath": r"/${id}-p${index}.${ext}",
|
||||||
"tagsWeight": "",
|
"tagsWeight": "",
|
||||||
"useTranslatedNameForDownload": false,
|
"useTranslatedNameForDownload": false,
|
||||||
|
"maxDownloadParallelism": 3
|
||||||
};
|
};
|
||||||
|
|
||||||
bool lock = false;
|
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(
|
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(),
|
||||||
|
@@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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,19 +35,25 @@ class _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{
|
|||||||
Widget buildTab() {
|
Widget buildTab() {
|
||||||
return TitleBar(
|
return TitleBar(
|
||||||
title: "Bookmarks".tl,
|
title: "Bookmarks".tl,
|
||||||
action: SegmentedButton(
|
action: Row(
|
||||||
options: [
|
children: [
|
||||||
SegmentedButtonOption("public", "Public".tl),
|
BatchDownloadButton(request: () => Network().getBookmarkedIllusts(restrict)),
|
||||||
SegmentedButtonOption("private", "Private".tl),
|
const SizedBox(width: 8,),
|
||||||
|
SegmentedButton(
|
||||||
|
options: [
|
||||||
|
SegmentedButtonOption("public", "Public".tl),
|
||||||
|
SegmentedButtonOption("private", "Private".tl),
|
||||||
|
],
|
||||||
|
onPressed: (key) {
|
||||||
|
if(key != restrict) {
|
||||||
|
setState(() {
|
||||||
|
restrict = key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: restrict,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
onPressed: (key) {
|
|
||||||
if(key != restrict) {
|
|
||||||
setState(() {
|
|
||||||
restrict = key;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
value: restrict,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,20 +35,26 @@ class _FollowingArtworksPageState extends State<FollowingArtworksPage> {
|
|||||||
Widget buildTab() {
|
Widget buildTab() {
|
||||||
return TitleBar(
|
return TitleBar(
|
||||||
title: "Following".tl,
|
title: "Following".tl,
|
||||||
action: SegmentedButton(
|
action: Row(
|
||||||
options: [
|
children: [
|
||||||
SegmentedButtonOption("all", "All".tl),
|
BatchDownloadButton(request: () => Network().getFollowingArtworks(restrict)),
|
||||||
SegmentedButtonOption("public", "Public".tl),
|
const SizedBox(width: 8,),
|
||||||
SegmentedButtonOption("private", "Private".tl),
|
SegmentedButton(
|
||||||
|
options: [
|
||||||
|
SegmentedButtonOption("all", "All".tl),
|
||||||
|
SegmentedButtonOption("public", "Public".tl),
|
||||||
|
SegmentedButtonOption("private", "Private".tl),
|
||||||
|
],
|
||||||
|
onPressed: (key) {
|
||||||
|
if(key != restrict) {
|
||||||
|
setState(() {
|
||||||
|
restrict = key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: restrict,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
onPressed: (key) {
|
|
||||||
if(key != restrict) {
|
|
||||||
setState(() {
|
|
||||||
restrict = key;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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/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));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
title,
|
child: Row(
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
children: [
|
||||||
).toAlign(Alignment.centerLeft)).paddingLeft(16).paddingVertical(4);
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
).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,
|
||||||
|
Reference in New Issue
Block a user