mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 04:57:23 +00:00
download and downloading page
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import 'foundation/app.dart';
|
import 'foundation/app.dart';
|
||||||
import 'network/models.dart';
|
import 'network/models.dart';
|
||||||
|
|
||||||
@@ -9,9 +12,18 @@ class _Appdata {
|
|||||||
|
|
||||||
var searchOptions = SearchOptions();
|
var searchOptions = SearchOptions();
|
||||||
|
|
||||||
|
Map<String, dynamic> settings = {
|
||||||
|
"downloadPath": null,
|
||||||
|
"downloadSubPath": r"/${id}-p${index}.${ext}",
|
||||||
|
"tagsWeight": "",
|
||||||
|
"useTranslatedNameForDownload": false,
|
||||||
|
};
|
||||||
|
|
||||||
void writeData() async {
|
void writeData() async {
|
||||||
await File("${App.dataPath}/account.json")
|
await File("${App.dataPath}/account.json")
|
||||||
.writeAsString(jsonEncode(account));
|
.writeAsString(jsonEncode(account));
|
||||||
|
await File("${App.dataPath}/settings.json")
|
||||||
|
.writeAsString(jsonEncode(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> readData() async {
|
Future<void> readData() async {
|
||||||
@@ -19,6 +31,39 @@ class _Appdata {
|
|||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
account = Account.fromJson(jsonDecode(await file.readAsString()));
|
account = Account.fromJson(jsonDecode(await file.readAsString()));
|
||||||
}
|
}
|
||||||
|
final settingsFile = File("${App.dataPath}/settings.json");
|
||||||
|
if (settingsFile.existsSync()) {
|
||||||
|
var json = jsonDecode(await settingsFile.readAsString());
|
||||||
|
for(var key in json.keys) {
|
||||||
|
settings[key] = json[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(settings["downloadPath"] == null) {
|
||||||
|
settings["downloadPath"] = await _defaultDownloadPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get downloadPath => settings["downloadPath"];
|
||||||
|
|
||||||
|
Future<String> get _defaultDownloadPath async{
|
||||||
|
if(App.isAndroid) {
|
||||||
|
var externalStoragePaths = await getExternalStorageDirectories(type: StorageDirectory.downloads);
|
||||||
|
var res = externalStoragePaths?.first.path;
|
||||||
|
res ??= (await getExternalStorageDirectory())!.path;
|
||||||
|
return "$res/pixes";
|
||||||
|
} else if (App.isWindows){
|
||||||
|
var res = await const MethodChannel("pixes/picture_folder").invokeMethod("");
|
||||||
|
if(res != "error") {
|
||||||
|
return res + "/pixes";
|
||||||
|
}
|
||||||
|
} else if (App.isMacOS || App.isLinux) {
|
||||||
|
var downloadPath = (await getDownloadsDirectory())?.path;
|
||||||
|
if(downloadPath != null) {
|
||||||
|
return "$downloadPath/pixes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${App.dataPath}/download";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -150,12 +150,16 @@ class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
|||||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async{
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async{
|
||||||
var cached = await CacheManager().findCache(key);
|
var cached = await CacheManager().findCache(key);
|
||||||
if(cached != null) {
|
if(cached != null) {
|
||||||
|
chunkEvents.add(const ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: 1,
|
||||||
|
expectedTotalBytes: 1,
|
||||||
|
));
|
||||||
return await File(cached).readAsBytes();
|
return await File(cached).readAsBytes();
|
||||||
}
|
}
|
||||||
var dio = AppDio();
|
var dio = AppDio();
|
||||||
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
||||||
var res = await dio.get(
|
var res = await dio.get<ResponseBody>(
|
||||||
url,
|
url,
|
||||||
options: Options(
|
options: Options(
|
||||||
responseType: ResponseType.stream,
|
responseType: ResponseType.stream,
|
||||||
@@ -174,12 +178,16 @@ class CachedImageProvider extends BaseImageProvider<CachedImageProvider> {
|
|||||||
}
|
}
|
||||||
var data = <int>[];
|
var data = <int>[];
|
||||||
var cachingFile = await CacheManager().openWrite(key);
|
var cachingFile = await CacheManager().openWrite(key);
|
||||||
await for (var chunk in res.data.stream) {
|
await for (var chunk in res.data!.stream) {
|
||||||
|
var length = res.data!.contentLength+1;
|
||||||
|
if(length < data.length) {
|
||||||
|
length = data.length + 1;
|
||||||
|
}
|
||||||
data.addAll(chunk);
|
data.addAll(chunk);
|
||||||
await cachingFile.writeBytes(chunk);
|
await cachingFile.writeBytes(chunk);
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
cumulativeBytesLoaded: data.length,
|
cumulativeBytesLoaded: data.length,
|
||||||
expectedTotalBytes: res.data.contentLength+1,
|
expectedTotalBytes: length,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
await cachingFile.close();
|
await cachingFile.close();
|
||||||
|
@@ -56,4 +56,12 @@ extension WidgetExtension on Widget{
|
|||||||
Widget sliverPaddingHorizontal(double padding){
|
Widget sliverPaddingHorizontal(double padding){
|
||||||
return SliverPadding(padding: EdgeInsets.symmetric(horizontal: padding), sliver: this);
|
return SliverPadding(padding: EdgeInsets.symmetric(horizontal: padding), sliver: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget fixWidth(double width){
|
||||||
|
return SizedBox(width: width, child: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget fixHeight(double height){
|
||||||
|
return SizedBox(height: height, child: this);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,268 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:pixes/appdata.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/log.dart';
|
||||||
|
import 'package:pixes/network/app_dio.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
|
import 'package:pixes/utils/io.dart';
|
||||||
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
|
|
||||||
extension IllustExt on Illust {
|
extension IllustExt on Illust {
|
||||||
bool get downloaded => false;
|
bool get downloaded => DownloadManager().checkDownloaded(id);
|
||||||
|
|
||||||
|
bool get downloading =>
|
||||||
|
DownloadManager().tasks.any((element) => element.illust.id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadedIllust {
|
||||||
|
final int illustId;
|
||||||
|
final String title;
|
||||||
|
final String author;
|
||||||
|
final int imageCount;
|
||||||
|
|
||||||
|
DownloadedIllust({
|
||||||
|
required this.illustId,
|
||||||
|
required this.title,
|
||||||
|
required this.author,
|
||||||
|
required this.imageCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadingTask {
|
||||||
|
final Illust illust;
|
||||||
|
|
||||||
|
void Function(int)? receiveBytesCallback;
|
||||||
|
|
||||||
|
void Function(DownloadingTask)? onCompleted;
|
||||||
|
|
||||||
|
DownloadingTask(this.illust, {this.receiveBytesCallback, this.onCompleted});
|
||||||
|
|
||||||
|
int _downloadingIndex = 0;
|
||||||
|
|
||||||
|
int get totalImages => illust.images.length;
|
||||||
|
|
||||||
|
int get downloadedImages => _downloadingIndex;
|
||||||
|
|
||||||
|
bool _stop = true;
|
||||||
|
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
_stop = false;
|
||||||
|
_download();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dio get dio => Network().dio;
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
_stop = true;
|
||||||
|
DownloadManager().tasks.remove(this);
|
||||||
|
for(var path in imagePaths) {
|
||||||
|
File(path).deleteIfExists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> imagePaths = [];
|
||||||
|
|
||||||
|
void _download() async{
|
||||||
|
try{
|
||||||
|
while(_downloadingIndex < illust.images.length) {
|
||||||
|
if(_stop) return;
|
||||||
|
var url = illust.images[_downloadingIndex].original;
|
||||||
|
var ext = url.split('.').last;
|
||||||
|
if(!["jpg", "png", "gif", "webp", "jpeg", "avif"].contains(ext)) {
|
||||||
|
ext = "jpg";
|
||||||
|
}
|
||||||
|
var path = _generateFilePath(illust, _downloadingIndex, ext);
|
||||||
|
final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
|
final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();
|
||||||
|
var res = await dio.get<ResponseBody>(url, options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
headers: {
|
||||||
|
"referer": "https://app-api.pixiv.net/",
|
||||||
|
"user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)",
|
||||||
|
"x-client-time": time,
|
||||||
|
"x-client-hash": hash,
|
||||||
|
"accept-enconding": "gzip",
|
||||||
|
},
|
||||||
|
));
|
||||||
|
var file = File(path);
|
||||||
|
if(!file.existsSync()) {
|
||||||
|
file.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
await for (var data in res.data!.stream) {
|
||||||
|
await file.writeAsBytes(data, mode: FileMode.append);
|
||||||
|
receiveBytesCallback?.call(data.length);
|
||||||
|
}
|
||||||
|
imagePaths.add(path);
|
||||||
|
_downloadingIndex++;
|
||||||
|
}
|
||||||
|
onCompleted?.call(this);
|
||||||
|
}
|
||||||
|
catch(e, s) {
|
||||||
|
error = e.toString();
|
||||||
|
_stop = true;
|
||||||
|
Log.error("Download", "Download error: $e\n$s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _generateFilePath(Illust illust, int index, String ext) {
|
||||||
|
final String downloadPath = appdata.settings["downloadPath"];
|
||||||
|
final String subPathPatten = appdata.settings["downloadSubPath"];
|
||||||
|
final tags = appdata.settings["useTranslatedNameForDownload"] == false
|
||||||
|
? illust.tags.map((e) => e.name).toList()
|
||||||
|
: illust.tags.map((e) => e.translatedName ?? e.name).toList();
|
||||||
|
final tagsWeight = (appdata.settings["tagsWeight"] as String).split(' ');
|
||||||
|
tags.sort((a, b) => tagsWeight.indexOf(a) - tagsWeight.indexOf(b));
|
||||||
|
subPathPatten.replaceAll(r"${id}", illust.id.toString());
|
||||||
|
subPathPatten.replaceAll(r"${title}", illust.title);
|
||||||
|
subPathPatten.replaceAll(r"${author}", illust.author.name);
|
||||||
|
subPathPatten.replaceAll(r"${ext}", ext);
|
||||||
|
for(int i=0; i<tags.length; i++) {
|
||||||
|
subPathPatten.replaceAll("\${tag$i}", tags[i]);
|
||||||
|
}
|
||||||
|
return "$downloadPath$subPathPatten";
|
||||||
|
}
|
||||||
|
|
||||||
|
void retry() {
|
||||||
|
error = null;
|
||||||
|
_stop = false;
|
||||||
|
_download();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadManager {
|
||||||
|
factory DownloadManager() => instance ??= DownloadManager._();
|
||||||
|
|
||||||
|
static DownloadManager? instance;
|
||||||
|
|
||||||
|
DownloadManager._(){
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
late Database _db;
|
||||||
|
|
||||||
|
int _currentBytes = 0;
|
||||||
|
int _bytesPerSecond = 0;
|
||||||
|
|
||||||
|
int get bytesPerSecond => _bytesPerSecond;
|
||||||
|
|
||||||
|
Timer? _loop;
|
||||||
|
|
||||||
|
var tasks = <DownloadingTask>[];
|
||||||
|
|
||||||
|
void Function()? uiUpdateCallback;
|
||||||
|
|
||||||
|
void registerUiUpdater(void Function() callback) {
|
||||||
|
uiUpdateCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUiUpdater() {
|
||||||
|
uiUpdateCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
_db = sqlite3.open("${App.dataPath}/download.db");
|
||||||
|
_db.execute('''
|
||||||
|
create table if not exists download (
|
||||||
|
illust_id integer primary key not null,
|
||||||
|
title text not null,
|
||||||
|
author text not null,
|
||||||
|
imageCount int not null
|
||||||
|
);
|
||||||
|
''');
|
||||||
|
_db.execute('''
|
||||||
|
create table if not exists images (
|
||||||
|
illust_id integer not null,
|
||||||
|
image_index integer not null,
|
||||||
|
path text not null,
|
||||||
|
primary key (illust_id, image_index)
|
||||||
|
);
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveInfo(Illust illust, List<String> imagePaths) {
|
||||||
|
_db.execute('''
|
||||||
|
insert into download (illust_id, title, author, imageCount)
|
||||||
|
values (?, ?, ?, ?)
|
||||||
|
''', [illust.id, illust.title, illust.author.name, imagePaths.length]);
|
||||||
|
for (var i = 0; i < imagePaths.length; i++) {
|
||||||
|
_db.execute('''
|
||||||
|
insert into images (illust_id, image_index, path)
|
||||||
|
values (?, ?, ?)
|
||||||
|
''', [illust.id, i, imagePaths[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File? getImage(int illustId, int index) {
|
||||||
|
var res = _db.select('''
|
||||||
|
select * from images
|
||||||
|
where illust_id = ? and image_index = ?;
|
||||||
|
''');
|
||||||
|
if (res.isEmpty) return null;
|
||||||
|
var file = File(res.first["path"] as String);
|
||||||
|
if (!file.existsSync()) return null;
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkDownloaded(int illustId) {
|
||||||
|
var res = _db.select('''
|
||||||
|
select * from download
|
||||||
|
where illust_id = ?;
|
||||||
|
''', [illustId]);
|
||||||
|
return res.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DownloadedIllust> listAll() {
|
||||||
|
var res = _db.select('''
|
||||||
|
select * from download;
|
||||||
|
''');
|
||||||
|
return res.map((e) =>
|
||||||
|
DownloadedIllust(
|
||||||
|
illustId: e["illust_id"] as int,
|
||||||
|
title: e["title"] as String,
|
||||||
|
author: e["author"] as String,
|
||||||
|
imageCount: e["imageCount"] as int,
|
||||||
|
)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDownloadingTask(Illust illust) {
|
||||||
|
var task = DownloadingTask(illust, receiveBytesCallback: receiveBytes, onCompleted: (task) {
|
||||||
|
saveInfo(illust, task.imagePaths);
|
||||||
|
tasks.remove(task);
|
||||||
|
});
|
||||||
|
tasks.add(task);
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
void receiveBytes(int bytes) {
|
||||||
|
_currentBytes += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get maxConcurrentTasks => 3;
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
_loop ??= Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
_bytesPerSecond = _currentBytes;
|
||||||
|
_currentBytes = 0;
|
||||||
|
uiUpdateCallback?.call();
|
||||||
|
for(int i=0; i<maxConcurrentTasks; i++) {
|
||||||
|
var task = tasks.elementAtOrNull(i);
|
||||||
|
if(task != null && task._stop && task.error == null) {
|
||||||
|
task.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(tasks.isEmpty) {
|
||||||
|
timer.cancel();
|
||||||
|
_loop = null;
|
||||||
|
_currentBytes = 0;
|
||||||
|
_bytesPerSecond = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@@ -39,7 +39,7 @@ class Network {
|
|||||||
|
|
||||||
final dio = AppDio();
|
final dio = AppDio();
|
||||||
|
|
||||||
Map<String, String> get _headers {
|
Map<String, String> get headers {
|
||||||
final time =
|
final time =
|
||||||
DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now());
|
||||||
final hash = md5.convert(utf8.encode(time + hashSalt)).toString();
|
final hash = md5.convert(utf8.encode(time + hashSalt)).toString();
|
||||||
@@ -80,7 +80,7 @@ class Network {
|
|||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
contentType: Headers.formUrlEncodedContentType,
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
headers: _headers));
|
headers: headers));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw "Invalid Status code ${res.statusCode}";
|
throw "Invalid Status code ${res.statusCode}";
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ class Network {
|
|||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
contentType: Headers.formUrlEncodedContentType,
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
headers: _headers));
|
headers: headers));
|
||||||
var account = Account.fromJson(json.decode(res.data!));
|
var account = Account.fromJson(json.decode(res.data!));
|
||||||
appdata.account = account;
|
appdata.account = account;
|
||||||
appdata.writeData();
|
appdata.writeData();
|
||||||
@@ -126,7 +126,7 @@ class Network {
|
|||||||
final res = await dio.get<Map<String, dynamic>>(path,
|
final res = await dio.get<Map<String, dynamic>>(path,
|
||||||
queryParameters: query,
|
queryParameters: query,
|
||||||
options:
|
options:
|
||||||
Options(headers: _headers, validateStatus: (status) => true));
|
Options(headers: headers, validateStatus: (status) => true));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
return Res(res.data!);
|
return Res(res.data!);
|
||||||
} else if (res.statusCode == 400) {
|
} else if (res.statusCode == 400) {
|
||||||
@@ -162,7 +162,7 @@ class Network {
|
|||||||
queryParameters: query,
|
queryParameters: query,
|
||||||
data: data,
|
data: data,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: _headers,
|
headers: headers,
|
||||||
validateStatus: (status) => true,
|
validateStatus: (status) => true,
|
||||||
contentType: Headers.formUrlEncodedContentType));
|
contentType: Headers.formUrlEncodedContentType));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
class DownloadPage extends StatefulWidget {
|
|
||||||
const DownloadPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DownloadPage> createState() => _DownloadPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DownloadPageState extends State<DownloadPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Placeholder();
|
|
||||||
}
|
|
||||||
}
|
|
15
lib/pages/downloaded_page.dart
Normal file
15
lib/pages/downloaded_page.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
|
class DownloadedPage extends StatefulWidget {
|
||||||
|
const DownloadedPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DownloadedPage> createState() => _DownloadedPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DownloadedPageState extends State<DownloadedPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
166
lib/pages/downloading_page.dart
Normal file
166
lib/pages/downloading_page.dart
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/components/md.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
import 'package:pixes/foundation/image_provider.dart';
|
||||||
|
import 'package:pixes/network/download.dart';
|
||||||
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
|
import '../utils/io.dart';
|
||||||
|
|
||||||
|
class DownloadingPage extends StatefulWidget {
|
||||||
|
const DownloadingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DownloadingPage> createState() => _DownloadingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DownloadingPageState extends State<DownloadingPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
DownloadManager().registerUiUpdater(() => setState((){}));
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
DownloadManager().removeUiUpdater();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, FlyoutController> controller = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ScaffoldPage(
|
||||||
|
content: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
buildTop(),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
||||||
|
buildContent()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTop() {
|
||||||
|
int bytesPerSecond = DownloadManager().bytesPerSecond;
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text("${"Speed".tl}: ${bytesToText(bytesPerSecond)}/s",
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildContent() {
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
var task = DownloadManager().tasks[index];
|
||||||
|
return buildItem(task);
|
||||||
|
},
|
||||||
|
childCount: DownloadManager().tasks.length
|
||||||
|
),
|
||||||
|
).sliverPaddingHorizontal(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(DownloadingTask task) {
|
||||||
|
controller[task.illust.id.toString()] ??= FlyoutController();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 96,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
height: double.infinity,
|
||||||
|
width: 72,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6),
|
||||||
|
),
|
||||||
|
child: Image(
|
||||||
|
image: CachedImageProvider(task.illust.images.first.medium),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(task.illust.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(task.illust.author.name, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
const Spacer(),
|
||||||
|
if(task.error == null)
|
||||||
|
Text("${task.downloadedImages}/${task.totalImages} ${"Downloaded".tl}", style: const TextStyle(fontSize: 12, color: Colors.grey))
|
||||||
|
else
|
||||||
|
Text("Error: ${task.error!.replaceAll("\n", " ")}", style: TextStyle(fontSize: 12, color: ColorScheme.of(context).error), maxLines: 2,),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if(task.error != null)
|
||||||
|
Button(
|
||||||
|
child: Text("Retry".tl).fixWidth(46),
|
||||||
|
onPressed: () {
|
||||||
|
task.retry();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
FlyoutTarget(
|
||||||
|
controller: controller[task.illust.id.toString()]!,
|
||||||
|
child: Button(
|
||||||
|
child: Text("Cancel".tl, style: TextStyle(color: ColorScheme.of(context).error),).fixWidth(46),
|
||||||
|
onPressed: (){
|
||||||
|
controller[task.illust.id.toString()]!.showFlyout(
|
||||||
|
navigatorKey: App.rootNavigatorKey.currentState,
|
||||||
|
builder: (context) {
|
||||||
|
return FlyoutContent(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Are you sure you want to cancel this download?'.tl,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
Button(
|
||||||
|
onPressed: () {
|
||||||
|
Flyout.of(context).close();
|
||||||
|
task.cancel();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Text('Yes'.tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:pixes/components/animated_image.dart';
|
import 'package:pixes/components/animated_image.dart';
|
||||||
@@ -69,6 +71,11 @@ class _IllustPageState extends State<IllustPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildImage(double width, double height, int index) {
|
Widget buildImage(double width, double height, int index) {
|
||||||
|
File? downloadFile;
|
||||||
|
if(widget.illust.downloaded) {
|
||||||
|
downloadFile = DownloadManager().getImage(widget.illust.id, index);
|
||||||
|
}
|
||||||
|
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Text(
|
return Text(
|
||||||
widget.illust.title,
|
widget.illust.title,
|
||||||
@@ -93,9 +100,13 @@ class _IllustPageState extends State<IllustPage> {
|
|||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
height: imageHeight,
|
height: imageHeight,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => ImagePage.show(widget.illust.images[index].original),
|
onTap: () => ImagePage.show(downloadFile == null
|
||||||
|
? widget.illust.images[index].original
|
||||||
|
: "file://${downloadFile.path}"),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedImageProvider(widget.illust.images[index].large),
|
image: downloadFile == null
|
||||||
|
? CachedImageProvider(widget.illust.images[index].large) as ImageProvider
|
||||||
|
: FileImage(downloadFile) as ImageProvider,
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
height: imageHeight,
|
height: imageHeight,
|
||||||
@@ -123,14 +134,9 @@ class _IllustPageState extends State<IllustPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index == 0) {
|
return Center(
|
||||||
return Hero(
|
child: image,
|
||||||
tag: "illust_${widget.illust.id}",
|
);
|
||||||
child: image,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +380,10 @@ class _BottomBarState extends State<_BottomBar> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void download() {}
|
void download() {
|
||||||
|
DownloadManager().addDownloadingTask(widget.illust);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
bool showText = width > 640;
|
bool showText = width > 640;
|
||||||
|
|
||||||
@@ -416,24 +425,47 @@ class _BottomBarState extends State<_BottomBar> {
|
|||||||
yield const SizedBox(width: 8,);
|
yield const SizedBox(width: 8,);
|
||||||
|
|
||||||
if (!widget.illust.downloaded) {
|
if (!widget.illust.downloaded) {
|
||||||
yield Button(
|
if(widget.illust.downloading) {
|
||||||
onPressed: download,
|
yield Button(
|
||||||
child: SizedBox(
|
onPressed: () => {},
|
||||||
height: 28,
|
child: SizedBox(
|
||||||
child: Row(
|
height: 28,
|
||||||
children: [
|
child: Row(
|
||||||
const Icon(
|
children: [
|
||||||
FluentIcons.download,
|
Icon(
|
||||||
size: 18,
|
FluentIcons.download,
|
||||||
),
|
color: ColorScheme.of(context).outline,
|
||||||
if(showText)
|
size: 18,
|
||||||
const SizedBox(width: 8,),
|
),
|
||||||
if(showText)
|
if(showText)
|
||||||
Text("Download".tl),
|
const SizedBox(width: 8,),
|
||||||
],
|
if(showText)
|
||||||
|
Text("Downloading".tl,
|
||||||
|
style: TextStyle(color: ColorScheme.of(context).outline),),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
yield Button(
|
||||||
|
onPressed: download,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
FluentIcons.download,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
if(showText)
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
if(showText)
|
||||||
|
Text("Download".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yield const SizedBox(width: 8,);
|
yield const SizedBox(width: 8,);
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:pixes/components/page_route.dart';
|
import 'package:pixes/components/page_route.dart';
|
||||||
@@ -60,7 +62,9 @@ class _ImagePageState extends State<ImagePage> with WindowListener{
|
|||||||
color: Colors.transparent
|
color: Colors.transparent
|
||||||
),
|
),
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
imageProvider: CachedImageProvider(widget.url),
|
imageProvider: widget.url.startsWith("file://")
|
||||||
|
? FileImage(File(widget.url.replaceFirst("file://", "")))
|
||||||
|
: CachedImageProvider(widget.url) as ImageProvider,
|
||||||
)),
|
)),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
|
@@ -8,6 +8,7 @@ import "package:pixes/components/md.dart";
|
|||||||
import "package:pixes/foundation/app.dart";
|
import "package:pixes/foundation/app.dart";
|
||||||
import "package:pixes/network/network.dart";
|
import "package:pixes/network/network.dart";
|
||||||
import "package:pixes/pages/bookmarks.dart";
|
import "package:pixes/pages/bookmarks.dart";
|
||||||
|
import "package:pixes/pages/downloaded_page.dart";
|
||||||
import "package:pixes/pages/following_artworks.dart";
|
import "package:pixes/pages/following_artworks.dart";
|
||||||
import "package:pixes/pages/ranking.dart";
|
import "package:pixes/pages/ranking.dart";
|
||||||
import "package:pixes/pages/recommendation_page.dart";
|
import "package:pixes/pages/recommendation_page.dart";
|
||||||
@@ -20,7 +21,7 @@ import "package:pixes/utils/translation.dart";
|
|||||||
import "package:window_manager/window_manager.dart";
|
import "package:window_manager/window_manager.dart";
|
||||||
|
|
||||||
import "../components/page_route.dart";
|
import "../components/page_route.dart";
|
||||||
import "download_page.dart";
|
import "downloading_page.dart";
|
||||||
|
|
||||||
const _kAppBarHeight = 36.0;
|
const _kAppBarHeight = 36.0;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class MainPage extends StatefulWidget {
|
|||||||
class _MainPageState extends State<MainPage> with WindowListener {
|
class _MainPageState extends State<MainPage> with WindowListener {
|
||||||
final navigatorKey = GlobalKey<NavigatorState>();
|
final navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
int index = 3;
|
int index = 4;
|
||||||
|
|
||||||
int windowButtonKey = 0;
|
int windowButtonKey = 0;
|
||||||
|
|
||||||
@@ -101,9 +102,14 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
title: Text('Search'.tl),
|
title: Text('Search'.tl),
|
||||||
body: const SizedBox.shrink(),
|
body: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
PaneItem(
|
||||||
|
icon: const Icon(MdIcons.downloading, size: 20,),
|
||||||
|
title: Text('Downloading'.tl),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
PaneItem(
|
PaneItem(
|
||||||
icon: const Icon(MdIcons.download, size: 20,),
|
icon: const Icon(MdIcons.download, size: 20,),
|
||||||
title: Text('Download'.tl),
|
title: Text('Downloaded'.tl),
|
||||||
body: const SizedBox.shrink(),
|
body: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
PaneItemSeparator(),
|
PaneItemSeparator(),
|
||||||
@@ -148,7 +154,8 @@ class _MainPageState extends State<MainPage> with WindowListener {
|
|||||||
static final pageBuilders = <Widget Function()>[
|
static final pageBuilders = <Widget Function()>[
|
||||||
() => UserInfoPage(appdata.account!.user.id),
|
() => UserInfoPage(appdata.account!.user.id),
|
||||||
() => const SearchPage(),
|
() => const SearchPage(),
|
||||||
() => const DownloadPage(),
|
() => const DownloadingPage(),
|
||||||
|
() => const DownloadedPage(),
|
||||||
() => const RecommendationPage(),
|
() => const RecommendationPage(),
|
||||||
() => const BookMarkedArtworkPage(),
|
() => const BookMarkedArtworkPage(),
|
||||||
() => const FollowingArtworksPage(),
|
() => const FollowingArtworksPage(),
|
||||||
|
@@ -20,3 +20,15 @@ extension FSExt on FileSystemEntity {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String bytesToText(int bytes) {
|
||||||
|
if(bytes < 1024) {
|
||||||
|
return "$bytes B";
|
||||||
|
} else if(bytes < 1024 * 1024) {
|
||||||
|
return "${(bytes / 1024).toStringAsFixed(2)} KB";
|
||||||
|
} else if(bytes < 1024 * 1024 * 1024) {
|
||||||
|
return "${(bytes / 1024 / 1024).toStringAsFixed(2)} MB";
|
||||||
|
} else {
|
||||||
|
return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB";
|
||||||
|
}
|
||||||
|
}
|
@@ -1,14 +1,46 @@
|
|||||||
|
#pragma comment(lib, "winhttp.lib")
|
||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
|
#include <flutter/method_channel.h>
|
||||||
#include <flutter/event_channel.h>
|
#include <flutter/event_channel.h>
|
||||||
#include <flutter/event_sink.h>
|
#include <flutter/event_sink.h>
|
||||||
#include <flutter/event_stream_handler_functions.h>
|
#include <flutter/event_stream_handler_functions.h>
|
||||||
#include <flutter/standard_method_codec.h>
|
#include <flutter/standard_method_codec.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <ShlObj.h>
|
||||||
|
#include <winhttp.h>
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& mouseEvents = nullptr;
|
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& mouseEvents = nullptr;
|
||||||
|
|
||||||
|
static std::string getProxy() {
|
||||||
|
_WINHTTP_CURRENT_USER_IE_PROXY_CONFIG net;
|
||||||
|
WinHttpGetIEProxyConfigForCurrentUser(&net);
|
||||||
|
if (net.lpszProxy == nullptr) {
|
||||||
|
GlobalFree(net.lpszAutoConfigUrl);
|
||||||
|
GlobalFree(net.lpszProxyBypass);
|
||||||
|
return "No Proxy";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GlobalFree(net.lpszAutoConfigUrl);
|
||||||
|
GlobalFree(net.lpszProxyBypass);
|
||||||
|
return Utf8FromUtf16(net.lpszProxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string getPicturePath() {
|
||||||
|
PWSTR picturesPath;
|
||||||
|
HRESULT result = SHGetKnownFolderPath(FOLDERID_Pictures, 0, NULL, &picturesPath);
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
auto res = Utf8FromUtf16(picturesPath);
|
||||||
|
CoTaskMemFree(picturesPath);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
: project_(project) {}
|
: project_(project) {}
|
||||||
|
|
||||||
@@ -31,15 +63,13 @@ bool FlutterWindow::OnCreate() {
|
|||||||
}
|
}
|
||||||
RegisterPlugins(flutter_controller_->engine());
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
|
||||||
//监听鼠标侧键的EventChannel
|
const auto channelName = "pixes/mouse";
|
||||||
const auto channelName = "pixes/mouse";
|
flutter::EventChannel<> channel2(
|
||||||
flutter::EventChannel<> channel2(
|
|
||||||
flutter_controller_->engine()->messenger(), channelName,
|
flutter_controller_->engine()->messenger(), channelName,
|
||||||
&flutter::StandardMethodCodec::GetInstance()
|
&flutter::StandardMethodCodec::GetInstance()
|
||||||
);
|
);
|
||||||
|
auto eventHandler = std::make_unique<
|
||||||
auto eventHandler = std::make_unique<
|
flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
|
||||||
flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
|
|
||||||
[](
|
[](
|
||||||
const flutter::EncodableValue* arguments,
|
const flutter::EncodableValue* arguments,
|
||||||
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events){
|
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events){
|
||||||
@@ -50,9 +80,27 @@ bool FlutterWindow::OnCreate() {
|
|||||||
-> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
|
-> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
|
||||||
mouseEvents = nullptr;
|
mouseEvents = nullptr;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
});
|
});
|
||||||
|
channel2.SetStreamHandler(std::move(eventHandler));
|
||||||
|
|
||||||
channel2.SetStreamHandler(std::move(eventHandler));
|
const auto pictureFolderChannel = "pixes/picture_folder";
|
||||||
|
flutter::MethodChannel<> channel3(
|
||||||
|
flutter_controller_->engine()->messenger(), pictureFolderChannel,
|
||||||
|
&flutter::StandardMethodCodec::GetInstance()
|
||||||
|
);
|
||||||
|
channel3.SetMethodCallHandler([](
|
||||||
|
const flutter::MethodCall<>& call, const std::unique_ptr<flutter::MethodResult<>>& result) {
|
||||||
|
result->Success(getPicturePath());
|
||||||
|
});
|
||||||
|
|
||||||
|
const flutter::MethodChannel<> channel(
|
||||||
|
flutter_controller_->engine()->messenger(), "pixes/proxy",
|
||||||
|
&flutter::StandardMethodCodec::GetInstance()
|
||||||
|
);
|
||||||
|
channel.SetMethodCallHandler(
|
||||||
|
[](const flutter::MethodCall<>& call, const std::unique_ptr<flutter::MethodResult<>>& result) {
|
||||||
|
result->Success(getProxy());
|
||||||
|
});
|
||||||
|
|
||||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user