diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3c8f758..680938a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,48 +1,49 @@
+ package="niuhuan.pikapi">
-
-
-
-
-
+
+
+
+
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:label="pikapi"
+ android:requestLegacyExternalStorage="true">
+
+ android:name=".MainActivity"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:windowSoftInputMode="adjustResize">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
+ android:name="io.flutter.embedding.android.SplashScreenDrawable"
+ android:resource="@drawable/launch_background" />
-
-
+
+
+ android:name="flutterEmbedding"
+ android:value="2" />
diff --git a/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt
index 3b22ab0..daf65f1 100644
--- a/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt
+++ b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt
@@ -13,6 +13,7 @@ import android.provider.MediaStore
import android.view.Display
import android.view.KeyEvent
import androidx.annotation.NonNull
+import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
@@ -22,7 +23,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.sync.Mutex
import mobile.Mobile
+import java.io.File
+import java.io.FileInputStream
import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.locks.Condition
+import java.util.concurrent.locks.ReentrantLock
class MainActivity : FlutterActivity() {
@@ -60,8 +66,12 @@ class MainActivity : FlutterActivity() {
}
}
+ private val resourceQueue: LinkedBlockingQueue = LinkedBlockingQueue()
+ private var cacheDir: String? = null
+
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
+ cacheDir = context!!.cacheDir.absolutePath
Mobile.initApplication(context!!.filesDir.absolutePath)
// Method Channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "method").setMethodCallHandler { call, result ->
@@ -86,6 +96,9 @@ class MainActivity : FlutterActivity() {
uiMode()
}
"androidGetVersion" -> Build.VERSION.SDK_INT
+// "exportComicDownloadAndroidQ" -> {
+// exportComicDownloadAndroidQ(call.argument("comicId")!!)
+// }
else -> {
notImplementedToken
}
@@ -194,7 +207,7 @@ class MainActivity : FlutterActivity() {
private fun setMode(string: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mixDisplay()?.let { display ->
- if (string == ""){
+ if (string == "") {
uiThreadHandler.post {
window.attributes = window.attributes.also { attr ->
attr.preferredDisplayModeId = 0
@@ -283,12 +296,63 @@ class MainActivity : FlutterActivity() {
}
}
+ // 安卓11以上使用了 MANAGE_EXTERNAL_STORAGE 权限来管理整个外置存储 (危险权限)
- fun main(){
- startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).also {
- it.addCategory(Intent.CATEGORY_OPENABLE)
- },1)
- }
-
+// private var tmpComicId: String? = null
+// private val exportComicDownloadAndroidQRequestCode = 2
+//
+// private fun exportComicDownloadAndroidQ(comicId: String) {
+// val title = Mobile.flatInvoke("specialDownloadTitle", comicId)
+// var fileName = title
+// fileName = fileName.replace('/', '_')
+// fileName = fileName.replace('\\', '_')
+// fileName = fileName.replace('*', '_')
+// fileName = fileName.replace('?', '_')
+// fileName = fileName.replace('<', '_')
+// fileName = fileName.replace('>', '_')
+// fileName = fileName.replace('|', '_')
+// fileName = fileName + "_" + System.currentTimeMillis() + ".zip"
+// tmpComicId = comicId
+// startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).also {
+// it.addCategory(Intent.CATEGORY_OPENABLE)
+// it.type = "application/octet-stream"
+// it.putExtra(Intent.EXTRA_TITLE, fileName)
+// }, exportComicDownloadAndroidQRequestCode)
+// val result = resourceQueue.take()
+// if (result is Throwable) {
+// throw result
+// }
+// return
+// }
+//
+// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+// pool.submit {
+// try {
+// if (resultCode === RESULT_OK && data != null) {
+// when (requestCode) {
+// exportComicDownloadAndroidQRequestCode -> {
+// contentResolver.openOutputStream(data.data!!)?.use { os ->
+// val path = Mobile.flatInvoke("exportComicDownload", Gson().toJson(HashMap().also { map ->
+// map["comicId"] = tmpComicId
+// map["dir"] = cacheDir
+// }))
+// try {
+// FileInputStream(path).copyTo(os)
+// } finally {
+// File(path).delete()
+// }
+// }
+// resourceQueue.put("OK")
+// }
+// else -> resourceQueue.put(Exception("WTF"))
+// }
+// } else {
+// resourceQueue.put(Exception("NOT OK"))
+// }
+// } catch (e: Throwable) {
+// resourceQueue.put(Exception(e))
+// }
+// }
+// }
}
diff --git a/go/pikapi/controller/export.go b/go/pikapi/controller/export.go
index eff5a46..ca88117 100644
--- a/go/pikapi/controller/export.go
+++ b/go/pikapi/controller/export.go
@@ -14,6 +14,7 @@ import (
"path"
"pgo/pikapi/const_value"
"pgo/pikapi/database/comic_center"
+ "strings"
"time"
)
@@ -67,7 +68,7 @@ func exportComicUsingSocketExit() error {
return nil
}
-func exportComicDownload(params string) error {
+func exportComicDownload(params string) (filePath string, err error) {
var paramsStruct struct {
ComicId string `json:"comicId"`
Dir string `json:"dir"`
@@ -78,29 +79,43 @@ func exportComicDownload(params string) error {
println(fmt.Sprintf("导出 %s 到 %s", comicId, dir))
comic, err := comic_center.FindComicDownloadById(comicId)
if err != nil {
- return err
+ return
}
if comic == nil {
- return errors.New("not found")
+ err = errors.New("not found")
+ return
}
if !comic.DownloadFinished {
- return errors.New("not download finish")
+ err = errors.New("not download finish")
+ return
}
- filePath := path.Join(dir, fmt.Sprintf("%s-%s.zip", comic.Title, time.Now().Format("2006_01_02_15_04_05.999")))
+ filePath = path.Join(dir, fmt.Sprintf("%s-%s.zip", reasonablePath(comic.Title), time.Now().Format("2006_01_02_15_04_05.999")))
println(fmt.Sprintf("ZIP : %s", filePath))
fileStream, err := os.Create(filePath)
if err != nil {
- return err
+ return
}
defer fileStream.Close()
zipWriter := zip.NewWriter(fileStream)
defer zipWriter.Close()
- return exportComicDownloadFetch(comicId, func(path string, size int64) (io.Writer, error) {
+ err = exportComicDownloadFetch(comicId, func(path string, size int64) (io.Writer, error) {
header := tar.Header{}
header.Name = path
header.Size = size
return zipWriter.Create(path)
})
+ return
+}
+
+func reasonablePath(title string) string {
+ title = strings.ReplaceAll(title, "\\", "_")
+ title = strings.ReplaceAll(title, "/", "_")
+ title = strings.ReplaceAll(title, "*", "_")
+ title = strings.ReplaceAll(title, "?", "_")
+ title = strings.ReplaceAll(title, "<", "_")
+ title = strings.ReplaceAll(title, ">", "_")
+ title = strings.ReplaceAll(title, "|", "_")
+ return title
}
func exportComicDownloadFetch(comicId string, onWriteFile func(path string, size int64) (io.Writer, error)) error {
@@ -367,7 +382,7 @@ func exportComicDownloadToJPG(params string) error {
if !comic.DownloadFinished {
return errors.New("not download finish")
}
- dirPath := path.Join(dir, fmt.Sprintf("%s-%s", comic.Title, time.Now().Format("2006_01_02_15_04_05.999")))
+ dirPath := path.Join(dir, fmt.Sprintf("%s-%s", reasonablePath(comic.Title), time.Now().Format("2006_01_02_15_04_05.999")))
println(fmt.Sprintf("DIR : %s", dirPath))
err = os.Mkdir(dirPath, const_value.CreateDirMode)
if err != nil {
diff --git a/go/pikapi/controller/pikapi.go b/go/pikapi/controller/pikapi.go
index 69d11c4..77b7f61 100644
--- a/go/pikapi/controller/pikapi.go
+++ b/go/pikapi/controller/pikapi.go
@@ -575,7 +575,7 @@ func FlatInvoke(method string, params string) (string, error) {
case "resetAllDownloads":
return "", comic_center.ResetAll()
case "exportComicDownload":
- return "", exportComicDownload(params)
+ return exportComicDownload(params)
case "exportComicDownloadToJPG":
return "", exportComicDownloadToJPG(params)
case "exportComicUsingSocket":
@@ -599,6 +599,8 @@ func FlatInvoke(method string, params string) (string, error) {
return downloadGame(params)
case "convertImageToJPEG100":
return "", convertImageToJPEG100(params)
+ case "specialDownloadTitle":
+ return specialDownloadTitle(params)
}
return "", errors.New("method not found : " + method)
}
diff --git a/go/pikapi/controller/special.go b/go/pikapi/controller/special.go
new file mode 100644
index 0000000..ed97d49
--- /dev/null
+++ b/go/pikapi/controller/special.go
@@ -0,0 +1,11 @@
+package controller
+
+import "pgo/pikapi/database/comic_center"
+
+func specialDownloadTitle(comicId string) (string, error) {
+ info, err := comic_center.DownloadInfo(comicId)
+ if err != nil {
+ return "", err
+ }
+ return info.Title, nil
+}
diff --git a/go/pikapi/database/comic_center/center.go b/go/pikapi/database/comic_center/center.go
index b89fb29..7690d0f 100644
--- a/go/pikapi/database/comic_center/center.go
+++ b/go/pikapi/database/comic_center/center.go
@@ -586,6 +586,20 @@ func DeleteRemoteImages(images []RemoteImage) error {
return db.Unscoped().Model(&RemoteImage{}).Delete("id in ?", ids).Error
}
+func DownloadInfo(comicId string) (*ComicDownload, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var download ComicDownload
+ err := db.First(&download, "id = ?", comicId).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &download, nil
+}
+
func VACUUM() error {
mutex.Lock()
defer mutex.Unlock()
diff --git a/lib/basic/Cross.dart b/lib/basic/Cross.dart
index 6b62373..2b07782 100644
--- a/lib/basic/Cross.dart
+++ b/lib/basic/Cross.dart
@@ -6,6 +6,7 @@ import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pikapi/basic/Common.dart';
+import 'package:pikapi/basic/config/Platform.dart';
import 'package:url_launcher/url_launcher.dart';
import 'Method.dart';
import 'config/ChooserRoot.dart';
@@ -57,6 +58,16 @@ Future saveImage(String path, BuildContext context) async {
}
}
+Future saveImageQuiet(String path, BuildContext context) async {
+ if (Platform.isIOS) {
+ return method.iosSaveFileToImage(path);
+ } else if (Platform.isAndroid) {
+ return _saveImageAndroid(path, context);
+ } else {
+ throw Exception("only mobile");
+ }
+}
+
Future _saveImageAndroid(String path, BuildContext context) async {
var p = await Permission.storage.request();
if (!p.isGranted) {
@@ -68,9 +79,14 @@ Future _saveImageAndroid(String path, BuildContext context) async {
/// 选择一个文件夹用于保存文件
Future chooseFolder(BuildContext context) async {
if (Platform.isAndroid) {
- var p = await Permission.storage.request();
- if (!p.isGranted) {
- return null;
+ if (androidVersion >= 30) {
+ if (!(await Permission.manageExternalStorage.request()).isGranted) {
+ return null;
+ }
+ } else {
+ if (!(await Permission.storage.request()).isGranted) {
+ return null;
+ }
}
}
return FilesystemPicker.open(
diff --git a/lib/basic/Method.dart b/lib/basic/Method.dart
index c1d0ffa..279bf15 100644
--- a/lib/basic/Method.dart
+++ b/lib/basic/Method.dart
@@ -429,6 +429,12 @@ class Method {
});
}
+ Future exportComicDownloadAndroidQ(String comicId) {
+ return _channel.invokeMethod("exportComicDownloadAndroidQ", {
+ "comicId": comicId,
+ });
+ }
+
Future exportComicDownloadToJPG(String comicId, String dir) {
return _flatInvoke("exportComicDownloadToJPG", {
"comicId": comicId,
diff --git a/lib/basic/config/ChooserRoot.dart b/lib/basic/config/ChooserRoot.dart
index 9846c8f..1d4197f 100644
--- a/lib/basic/config/ChooserRoot.dart
+++ b/lib/basic/config/ChooserRoot.dart
@@ -23,7 +23,10 @@ String currentChooserRoot() {
} else if (Platform.isLinux) {
return '/';
} else if (Platform.isAndroid) {
- return '/storage/emulated/0/Download';
+ // if (androidVersion >= 30) {
+ // return '/storage/emulated/0/Download';
+ // }
+ return '/storage/emulated/0';
} else {
throw 'error';
}
diff --git a/lib/basic/config/Platform.dart b/lib/basic/config/Platform.dart
new file mode 100644
index 0000000..632ab51
--- /dev/null
+++ b/lib/basic/config/Platform.dart
@@ -0,0 +1,12 @@
+
+import 'dart:io';
+
+import '../Method.dart';
+
+int androidVersion = 0;
+
+Future initPlatform()async{
+ if (Platform.isAndroid) {
+ androidVersion = await method.androidGetVersion();
+ }
+}
\ No newline at end of file
diff --git a/lib/basic/config/Themes.dart b/lib/basic/config/Themes.dart
index 93325ee..2dbe5f5 100644
--- a/lib/basic/config/Themes.dart
+++ b/lib/basic/config/Themes.dart
@@ -6,6 +6,7 @@ import 'package:event/event.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../Method.dart';
+import 'Platform.dart';
// 主题包
abstract class _ThemePackage {
@@ -124,7 +125,6 @@ final _themePackages = <_ThemePackage>[
// 主题更换事件
var themeEvent = Event();
-int _androidVersion = 1;
String? _themeCode;
ThemeData? _themeData;
bool _androidNightMode = false;
@@ -161,18 +161,15 @@ void _changeThemeByCode(String themeCode) {
const _nightModePropertyName = "androidNightMode";
Future initTheme() async {
- if (Platform.isAndroid) {
- _androidVersion = await method.androidGetVersion();
- if (_androidVersion >= 29) {
- _androidNightMode =
- (await method.loadProperty(_nightModePropertyName, "false")) ==
- "true";
- _systemNight = (await method.androidGetUiMode()) == "NIGHT";
- EventChannel("ui_mode").receiveBroadcastStream().listen((event) {
- _systemNight = "$event" == "NIGHT";
- themeEvent.broadcast();
- });
- }
+ if (androidVersion >= 29) {
+ _androidNightMode =
+ (await method.loadProperty(_nightModePropertyName, "false")) ==
+ "true";
+ _systemNight = (await method.androidGetUiMode()) == "NIGHT";
+ EventChannel("ui_mode").receiveBroadcastStream().listen((event) {
+ _systemNight = "$event" == "NIGHT";
+ themeEvent.broadcast();
+ });
}
_changeThemeByCode(await method.loadTheme());
}
@@ -185,7 +182,7 @@ Future chooseTheme(BuildContext buildContext) async {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
var list = [];
- if (_androidVersion >= 29) {
+ if (androidVersion >= 29) {
var onChange = (bool? v) async {
if (v != null) {
await method.saveProperty(
@@ -206,10 +203,10 @@ Future chooseTheme(BuildContext buildContext) async {
decoration: BoxDecoration(
border: Border(
top: BorderSide(
- color: Theme
- .of(context)
- .dividerColor,
- width: 0.5,
+ color: Theme
+ .of(context)
+ .dividerColor,
+ width: 0.5,
),
bottom: BorderSide(
color: Theme
diff --git a/lib/screens/DownloadExportToFileScreen.dart b/lib/screens/DownloadExportToFileScreen.dart
index 2eb41f6..5a5ffba 100644
--- a/lib/screens/DownloadExportToFileScreen.dart
+++ b/lib/screens/DownloadExportToFileScreen.dart
@@ -2,10 +2,13 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:pikapi/basic/Channels.dart';
+import 'package:pikapi/basic/Common.dart';
import 'package:pikapi/basic/Cross.dart';
import 'package:pikapi/basic/Entities.dart';
import 'package:pikapi/basic/Method.dart';
+import 'package:pikapi/basic/config/Platform.dart';
import 'package:pikapi/screens/DownloadExportToSocketScreen.dart';
import 'components/ContentError.dart';
@@ -117,74 +120,118 @@ class _DownloadExportToFileScreenState
}
List _buildExportToFileButtons() {
+ List widgets = [];
if (Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isAndroid) {
- return [
- MaterialButton(
- onPressed: () async {
- String? path = await chooseFolder(context);
- print("path $path");
- if (path != null) {
- try {
- setState(() {
- exporting = true;
- });
- await method.exportComicDownloadToJPG(
- widget.comicId,
- path,
- );
- setState(() {
- exportResult = "导出成功";
- });
- } catch (e) {
- setState(() {
- exportResult = "导出失败 $e";
- });
- } finally {
- setState(() {
- exporting = false;
- });
- }
+ widgets.add(MaterialButton(
+ onPressed: () async {
+ String? path = await chooseFolder(context);
+ print("path $path");
+ if (path != null) {
+ try {
+ setState(() {
+ exporting = true;
+ });
+ await method.exportComicDownloadToJPG(
+ widget.comicId,
+ path,
+ );
+ setState(() {
+ exportResult = "导出成功";
+ });
+ } catch (e) {
+ setState(() {
+ exportResult = "导出失败 $e";
+ });
+ } finally {
+ setState(() {
+ exporting = false;
+ });
}
- },
- child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'),
- ),
- Container(height: 10),
- MaterialButton(
- onPressed: () async {
- String? path = await chooseFolder(context);
- print("path $path");
- if (path != null) {
- try {
- setState(() {
- exporting = true;
- });
- await method.exportComicDownload(
- widget.comicId,
- path,
- );
- setState(() {
- exportResult = "导出成功";
- });
- } catch (e) {
- setState(() {
- exportResult = "导出失败 $e";
- });
- } finally {
- setState(() {
- exporting = false;
- });
- }
+ }
+ },
+ child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'),
+ ));
+ widgets.add(Container(height: 10));
+ widgets.add(MaterialButton(
+ onPressed: () async {
+ String? path = await chooseFolder(context);
+ print("path $path");
+ if (path != null) {
+ try {
+ setState(() {
+ exporting = true;
+ });
+ await method.exportComicDownload(
+ widget.comicId,
+ path,
+ );
+ setState(() {
+ exportResult = "导出成功";
+ });
+ } catch (e) {
+ setState(() {
+ exportResult = "导出失败 $e";
+ });
+ } finally {
+ setState(() {
+ exporting = false;
+ });
}
- },
- child: _buildButtonInner('导出到HTML.zip\n(可从其他设备导入 / 解压后可阅读)'),
- ),
- Container(height: 10),
- ];
+ }
+ },
+ child: _buildButtonInner('导出到HTML.zip\n(可从其他设备导入 / 解压后可阅读)'),
+ ));
+ widgets.add(Container(height: 10));
}
- return [];
+ if (Platform.isIOS || Platform.isAndroid) {
+ widgets.add(MaterialButton(
+ onPressed: () async {
+ if (!(await confirmDialog(context, "导出确认", "将本漫画所有图片到相册?"))) {
+ return;
+ }
+ if (!(await Permission.storage.request()).isGranted) {
+ return null;
+ }
+ try {
+ setState(() {
+ exporting = true;
+ });
+ // 导出所有图片数据
+ var count = 0;
+ List eps = await method.downloadEpList(widget.comicId);
+ for (var i = 0; i < eps.length; i++) {
+ var pics = await method.downloadPicturesByEpId(eps[i].id);
+ for (var j = 0; j < pics.length; j++) {
+ setState(() {
+ exportMessage = "导出图片 ${count++} 张";
+ });
+ await saveImageQuiet(
+ await method.downloadImagePath(pics[j].localPath),
+ context,
+ );
+ }
+ }
+ setState(() {
+ exportResult = "导出成功";
+ });
+ } catch (e) {
+ setState(() {
+ exportResult = "导出失败 $e";
+ });
+ } finally {
+ setState(() {
+ exporting = false;
+ });
+ }
+ },
+ child: _buildButtonInner('将所有图片到处到手机相机'),
+ ));
+ widgets.add(Container(height: 10));
+ }
+ return widgets;
}
Widget _buildButtonInner(String text) {
diff --git a/lib/screens/InitScreen.dart b/lib/screens/InitScreen.dart
index 878f2cc..282840c 100644
--- a/lib/screens/InitScreen.dart
+++ b/lib/screens/InitScreen.dart
@@ -9,6 +9,7 @@ import 'package:pikapi/basic/config/FullScreenAction.dart';
import 'package:pikapi/basic/config/FullScreenUI.dart';
import 'package:pikapi/basic/config/KeyboardController.dart';
import 'package:pikapi/basic/config/PagerAction.dart';
+import 'package:pikapi/basic/config/Platform.dart';
import 'package:pikapi/basic/config/Proxy.dart';
import 'package:pikapi/basic/config/Quality.dart';
import 'package:pikapi/basic/config/ReaderDirection.dart';
@@ -37,6 +38,7 @@ class _InitScreenState extends State {
Future _init() async {
// 初始化配置文件
+ await initPlatform(); // 必须第一个初始化, 加载设备信息
await autoClean();
await initAddress();
await initProxy();