mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-16 10:54:14 +08:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -17,7 +17,9 @@ class GalleryBasicInfo extends StatelessWidget {
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: Row(children: [
|
||||
Expanded(flex: 2, child: Thumbnail(firstPage, fileId: fileId)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Thumbnail(firstPage, fileId: fileId, gid: gMeta.gid)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
|
||||
@@ -67,9 +67,12 @@ class _GalleryInfo extends State<GalleryInfo> with ThemeModeWidget {
|
||||
fileId: firstFileId, controller: controller),
|
||||
]),
|
||||
),
|
||||
ThumbnailGridView(widget.gData.pages,
|
||||
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: useMobile ? 2 : 5),
|
||||
files: widget.files),
|
||||
ThumbnailGridView(
|
||||
widget.gData.pages,
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: useMobile ? 2 : 5),
|
||||
files: widget.files,
|
||||
gid: widget.gData.meta.gid),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ class GalleryInfoDesktop extends StatelessWidget {
|
||||
width: 1280,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
flex: 3, child: Thumbnail(gData.pages.first, fileId: fileId)),
|
||||
flex: 3,
|
||||
child: Thumbnail(gData.pages.first,
|
||||
fileId: fileId, gid: gData.meta.gid)),
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Column(
|
||||
|
||||
@@ -3,17 +3,20 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
import '../globals.dart';
|
||||
import '../utils/clipboard.dart';
|
||||
|
||||
final _log = Logger("ImageWithContextMenu");
|
||||
|
||||
class ImageWithContextMenu extends StatelessWidget {
|
||||
const ImageWithContextMenu(this.data,
|
||||
{Key? key, this.uri, this.fmt = ImageFmt.jpg})
|
||||
{Key? key, this.uri, this.dir, this.fileName, this.fmt = ImageFmt.jpg})
|
||||
: super(key: key);
|
||||
final Uint8List data;
|
||||
final String? uri;
|
||||
final ImageFmt fmt;
|
||||
final String? fileName;
|
||||
final String? dir;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContextMenuWidget(
|
||||
@@ -38,6 +41,18 @@ class ImageWithContextMenu extends StatelessWidget {
|
||||
});
|
||||
}));
|
||||
}
|
||||
if (fileName != null) {
|
||||
list.add(MenuAction(
|
||||
title: AppLocalizations.of(context)!.saveAs,
|
||||
callback: () {
|
||||
try {
|
||||
platformPath.saveFile(fileName!, fmt.toMimeType(), data,
|
||||
dir: dir ?? "");
|
||||
} catch (err) {
|
||||
_log.warning("Failed to save image:", err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
return Menu(children: list);
|
||||
},
|
||||
child: Image.memory(data));
|
||||
|
||||
@@ -4,9 +4,11 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
import '../api/client.dart';
|
||||
import '../api/gallery.dart';
|
||||
import '../globals.dart';
|
||||
import '../utils.dart';
|
||||
import '../utils/clipboard.dart';
|
||||
import 'image.dart';
|
||||
|
||||
@@ -14,7 +16,7 @@ final _log = Logger("Thumbnail");
|
||||
|
||||
class Thumbnail extends StatefulWidget {
|
||||
const Thumbnail(ExtendedPMeta pMeta,
|
||||
{Key? key, int? max, int? width, int? height, int? fileId})
|
||||
{Key? key, int? max, int? width, int? height, int? fileId, this.gid})
|
||||
: _pMeta = pMeta,
|
||||
_max = max ?? 1200,
|
||||
_width = width,
|
||||
@@ -26,6 +28,7 @@ class Thumbnail extends StatefulWidget {
|
||||
final int? _width;
|
||||
final int? _height;
|
||||
final int? _fileId;
|
||||
final int? gid;
|
||||
|
||||
int get height => _height != null
|
||||
? _height!
|
||||
@@ -45,6 +48,7 @@ class Thumbnail extends StatefulWidget {
|
||||
enum _ThumbnailMenu {
|
||||
copyImage,
|
||||
copyImgUrl,
|
||||
saveAs,
|
||||
}
|
||||
|
||||
class _Thumbnail extends State<Thumbnail> {
|
||||
@@ -55,6 +59,8 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
bool _showNsfw = false;
|
||||
String? _uri;
|
||||
CancelToken? _cancel;
|
||||
String? _fileName;
|
||||
String _dir = "";
|
||||
Future<void> _fetchData() async {
|
||||
try {
|
||||
_cancel = CancelToken();
|
||||
@@ -106,6 +112,8 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
_fileId = widget._fileId;
|
||||
_showNsfw = false;
|
||||
_uri = null;
|
||||
_fileName = "${basenameWithoutExtension(widget._pMeta.name)}_thumb";
|
||||
_dir = isAndroid && widget.gid != null ? widget.gid!.toString() : "";
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -133,6 +141,14 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
_log.warning("Failed to copy image url to clipboard:", err);
|
||||
}
|
||||
break;
|
||||
case _ThumbnailMenu.saveAs:
|
||||
try {
|
||||
await platformPath.saveFile(_fileName!, "image/jpeg", _data!,
|
||||
dir: _dir);
|
||||
} catch (err) {
|
||||
_log.warning("Failed to save image:", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,14 +157,16 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
final isLoading = _data == null && _error == null;
|
||||
final isNsfw = widget._pMeta.isNsfw;
|
||||
if (isLoading && !_isLoading) _fetchData();
|
||||
final iconSize = Theme.of(context).iconTheme.size;
|
||||
final iconSize = MediaQuery.of(context).size.width < 400
|
||||
? 14.0
|
||||
: Theme.of(context).iconTheme.size;
|
||||
final moreVertMenu = Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
child: PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
child: Icon(Icons.more_vert, size: iconSize),
|
||||
onSelected: (v) {
|
||||
onItemSelected(v);
|
||||
},
|
||||
@@ -160,6 +178,9 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
PopupMenuItem(
|
||||
value: _ThumbnailMenu.copyImgUrl,
|
||||
child: Text(AppLocalizations.of(context)!.copyImgUrl)),
|
||||
PopupMenuItem(
|
||||
value: _ThumbnailMenu.saveAs,
|
||||
child: Text(AppLocalizations.of(context)!.saveAs)),
|
||||
];
|
||||
return list;
|
||||
}));
|
||||
@@ -180,8 +201,10 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
tileMode: TileMode.decal),
|
||||
child:
|
||||
ImageWithContextMenu(_data!, uri: _uri))),
|
||||
child: ImageWithContextMenu(_data!,
|
||||
uri: _uri,
|
||||
fileName: _fileName,
|
||||
dir: _dir))),
|
||||
SizedBox(
|
||||
width: widget.width.toDouble(),
|
||||
height: widget.height.toDouble(),
|
||||
@@ -202,7 +225,8 @@ class _Thumbnail extends State<Thumbnail> {
|
||||
SizedBox(
|
||||
width: widget.width.toDouble(),
|
||||
height: widget.height.toDouble(),
|
||||
child: ImageWithContextMenu(_data!, uri: _uri)),
|
||||
child: ImageWithContextMenu(_data!,
|
||||
uri: _uri, fileName: _fileName, dir: _dir)),
|
||||
moreVertMenu
|
||||
])
|
||||
: Center(
|
||||
|
||||
@@ -5,9 +5,11 @@ import '../globals.dart';
|
||||
import 'thumbnail.dart';
|
||||
|
||||
class ThumbnailGridView extends StatelessWidget {
|
||||
const ThumbnailGridView(this.pages, this.gridDelegate, {Key? key, this.files})
|
||||
const ThumbnailGridView(this.pages, this.gridDelegate,
|
||||
{Key? key, this.files, this.gid})
|
||||
: super(key: key);
|
||||
final List<ExtendedPMeta> pages;
|
||||
final int? gid;
|
||||
final EhFiles? files;
|
||||
final SliverGridDelegate gridDelegate;
|
||||
|
||||
@@ -24,7 +26,7 @@ class ThumbnailGridView extends StatelessWidget {
|
||||
files != null ? files!.files[page.token]!.first.id : null;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Thumbnail(page, fileId: fileId));
|
||||
child: Thumbnail(page, fileId: fileId, gid: gid));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class _GalleriesPage extends State<GalleriesPage> with ThemeModeWidget {
|
||||
}
|
||||
},
|
||||
label: Text(AppLocalizations.of(context)!.sortByGid,
|
||||
style: Theme.of(context).primaryTextTheme.labelMedium),
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
dropdownMenuEntries: [
|
||||
DropdownMenuEntry(
|
||||
value: SortByGid.none,
|
||||
|
||||
@@ -35,5 +35,6 @@
|
||||
"none": "none",
|
||||
"sortByGid": "Sort by gallery id",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending"
|
||||
"desc": "Descending",
|
||||
"saveAs": "Save As"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,6 @@
|
||||
"none": "无",
|
||||
"sortByGid": "按画廊ID排序",
|
||||
"asc": "升序",
|
||||
"desc": "降序"
|
||||
"desc": "降序",
|
||||
"saveAs": "另存为"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import '../utils.dart';
|
||||
import 'save_file.dart';
|
||||
|
||||
final Logger _log = Logger("platformPath");
|
||||
|
||||
class Path {
|
||||
static const platform = MethodChannel("lifegpc.eh_downloader_flutter/path");
|
||||
static const _safChannel=MethodChannel("lifegpc.eh_downloader_flutter/saf");
|
||||
static const _safChannel = MethodChannel("lifegpc.eh_downloader_flutter/saf");
|
||||
String? _currentExe;
|
||||
bool _currentExeLoaded = false;
|
||||
|
||||
@@ -28,7 +30,13 @@ class Path {
|
||||
}
|
||||
|
||||
/// 保存文件
|
||||
static Future<void> saveFile(String filenameWithoutExtension,String mimeType,Uint8List bytes,{String dir=""}) async{
|
||||
return _safChannel.invokeMethod("saveFile",[filenameWithoutExtension,dir,mimeType,bytes]);
|
||||
Future<void> saveFile(
|
||||
String filenameWithoutExtension, String mimeType, Uint8List bytes,
|
||||
{String dir = ""}) async {
|
||||
if (kIsWeb) {
|
||||
return saveFileWeb(bytes, mimeType, filenameWithoutExtension);
|
||||
}
|
||||
return _safChannel.invokeMethod(
|
||||
"saveFile", [filenameWithoutExtension, dir, mimeType, bytes]);
|
||||
}
|
||||
}
|
||||
|
||||
1
lib/platform/save_file.dart
Normal file
1
lib/platform/save_file.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'save_file_none.dart' if (dart.library.html) 'save_file_web.dart';
|
||||
6
lib/platform/save_file_none.dart
Normal file
6
lib/platform/save_file_none.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
void saveFileWeb(
|
||||
Uint8List data, String mimeType, String filenameWithoutExtension) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
28
lib/platform/save_file_web.dart
Normal file
28
lib/platform/save_file_web.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html';
|
||||
import 'dart:typed_data';
|
||||
|
||||
void saveFileWeb(
|
||||
Uint8List data, String mimeType, String filenameWithoutExtension) {
|
||||
final blob = Blob([data], mimeType);
|
||||
final url = Url.createObjectUrlFromBlob(blob);
|
||||
final a = document.createElement("a") as AnchorElement;
|
||||
a.href = url;
|
||||
var ext = "";
|
||||
switch (mimeType) {
|
||||
case "image/jpeg":
|
||||
ext = ".jpg";
|
||||
break;
|
||||
case "image/png":
|
||||
ext = ".png";
|
||||
break;
|
||||
case "image/gif":
|
||||
ext = ".gif";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
a.download = "$filenameWithoutExtension$ext";
|
||||
a.click();
|
||||
Url.revokeObjectUrl(url);
|
||||
}
|
||||
@@ -6,7 +6,18 @@ import '../platform/to_png_none.dart'
|
||||
enum ImageFmt {
|
||||
jpg,
|
||||
png,
|
||||
gif,
|
||||
gif;
|
||||
|
||||
String toMimeType() {
|
||||
switch (this) {
|
||||
case ImageFmt.jpg:
|
||||
return "image/jpeg";
|
||||
case ImageFmt.png:
|
||||
return "image/png";
|
||||
case ImageFmt.gif:
|
||||
return "image/gif";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> copyImageToClipboard(Uint8List data, ImageFmt fmt) async {
|
||||
|
||||
@@ -39,6 +39,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE utils)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "Comdlg32.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
|
||||
@@ -58,6 +58,71 @@ bool FlutterWindow::OnCreate() {
|
||||
result->NotImplemented();
|
||||
}
|
||||
});
|
||||
flutter::MethodChannel<> saf(flutter_controller_->engine()->messenger(), "lifegpc.eh_downloader_flutter/saf",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
saf.SetMethodCallHandler([&](const flutter::MethodCall<>& call, std::unique_ptr<flutter::MethodResult<>> result) {
|
||||
if (call.method_name() == "saveFile") {
|
||||
auto args = std::get_if<flutter::EncodableList>(call.arguments());
|
||||
auto fileName = std::get_if<std::string>(&args->at(0));
|
||||
auto dir = std::get_if<std::string>(&args->at(1));
|
||||
auto mimeType = std::get_if<std::string>(&args->at(2));
|
||||
auto data = std::get_if<std::vector<uint8_t>>(&args->at(3));
|
||||
if (!fileName || !dir || !mimeType || !data) {
|
||||
result->Error("INVALID_ARGUMENT", "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
std::wstring wFileName;
|
||||
if (!wchar_util::str_to_wstr(wFileName, *fileName, CP_UTF8)) {
|
||||
result->Error("ERROR", "Failed to convert file name to wstring.");
|
||||
return;
|
||||
}
|
||||
std::wstring wDir;
|
||||
if (!dir->empty() && !wchar_util::str_to_wstr(wDir, *dir, CP_UTF8)) {
|
||||
result->Error("ERROR", "Failed to convert dir to wstring.");
|
||||
return;
|
||||
}
|
||||
OPENFILENAMEW ofn;
|
||||
ZeroMemory(&ofn, sizeof(ofn));
|
||||
ofn.lStructSize = sizeof(ofn);
|
||||
ofn.hwndOwner = Win32Window::GetHandle();
|
||||
std::wstring filter;
|
||||
std::wstring defExt;
|
||||
if (*mimeType == "image/jpeg") {
|
||||
filter.append(std::wstring(L"JPEG File(*.jpg)\0*.jpg\0", 23));
|
||||
defExt = L"jpg";
|
||||
} else if (*mimeType == "image/png") {
|
||||
filter.append(std::wstring(L"PNG File(*.png)\0*.png\0", 22));
|
||||
defExt = L"png";
|
||||
} else if (*mimeType == "image/gif") {
|
||||
filter.append(std::wstring(L"GIF File(*.gif)\0*.gif\0", 22));
|
||||
defExt = L"gif";
|
||||
};
|
||||
filter.append(std::wstring(L"All Files\0*.*\0\0", 15));
|
||||
ofn.lpstrFilter = filter.c_str();
|
||||
ofn.lpstrDefExt = defExt.empty() ? nullptr : defExt.c_str();
|
||||
wchar_t wFileNameBuf[MAX_PATH_SIZE];
|
||||
memcpy(wFileNameBuf, wFileName.c_str(), (wFileName.size() + 1) * sizeof(wchar_t));
|
||||
ofn.lpstrFile = wFileNameBuf;
|
||||
ofn.nMaxFile = MAX_PATH_SIZE;
|
||||
ofn.lpstrInitialDir = wDir.empty() ? nullptr : wDir.c_str();
|
||||
ofn.Flags = OFN_DONTADDTORECENT | OFN_NONETWORKBUTTON | OFN_NOREADONLYRETURN | OFN_OVERWRITEPROMPT;
|
||||
if (!GetSaveFileNameW(&ofn)) {
|
||||
result->Error("ERROR", "Failed to get file name.");
|
||||
return;
|
||||
}
|
||||
FILE* f = nullptr;
|
||||
_wfopen_s(&f, wFileNameBuf, L"wb");
|
||||
if (!f) {
|
||||
result->Error("ERROR", "Failed to open file.");
|
||||
return;
|
||||
}
|
||||
fwrite(data->data(), sizeof(uint8_t), data->size(), f);
|
||||
fclose(f);
|
||||
result->Success();
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
});
|
||||
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user