export all images to albums

This commit is contained in:
niuhuan 2021-10-14 18:12:36 +08:00
parent 9e2027ef45
commit e3af9c25f1
13 changed files with 313 additions and 123 deletions

View File

@ -1,21 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="niuhuan.pikapi">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:icon="@mipmap/ic_launcher"
android:label="pikapi"
android:icon="@mipmap/ic_launcher">
android:requestLegacyExternalStorage="true">
<!-- requestLegacyExternalStorage="true" api29 down -->
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
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">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@ -23,8 +26,7 @@
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme" />
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
@ -32,17 +34,16 @@
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
android:resource="@drawable/launch_background" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
android:value="2" />
</application>
</manifest>

View File

@ -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<Any?> = 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<Any, Any?>().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))
// }
// }
// }
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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<dynamic> saveImage(String path, BuildContext context) async {
}
}
Future<dynamic> 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<dynamic> _saveImageAndroid(String path, BuildContext context) async {
var p = await Permission.storage.request();
if (!p.isGranted) {
@ -68,10 +79,15 @@ Future<dynamic> _saveImageAndroid(String path, BuildContext context) async {
///
Future<String?> chooseFolder(BuildContext context) async {
if (Platform.isAndroid) {
var p = await Permission.storage.request();
if (!p.isGranted) {
if (androidVersion >= 30) {
if (!(await Permission.manageExternalStorage.request()).isGranted) {
return null;
}
} else {
if (!(await Permission.storage.request()).isGranted) {
return null;
}
}
}
return FilesystemPicker.open(
title: '选择一个文件夹',

View File

@ -429,6 +429,12 @@ class Method {
});
}
Future<dynamic> exportComicDownloadAndroidQ(String comicId) {
return _channel.invokeMethod("exportComicDownloadAndroidQ", {
"comicId": comicId,
});
}
Future<dynamic> exportComicDownloadToJPG(String comicId, String dir) {
return _flatInvoke("exportComicDownloadToJPG", {
"comicId": comicId,

View File

@ -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';
}

View File

@ -0,0 +1,12 @@
import 'dart:io';
import '../Method.dart';
int androidVersion = 0;
Future<void> initPlatform()async{
if (Platform.isAndroid) {
androidVersion = await method.androidGetVersion();
}
}

View File

@ -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<EventArgs>();
int _androidVersion = 1;
String? _themeCode;
ThemeData? _themeData;
bool _androidNightMode = false;
@ -161,9 +161,7 @@ void _changeThemeByCode(String themeCode) {
const _nightModePropertyName = "androidNightMode";
Future<dynamic> initTheme() async {
if (Platform.isAndroid) {
_androidVersion = await method.androidGetVersion();
if (_androidVersion >= 29) {
if (androidVersion >= 29) {
_androidNightMode =
(await method.loadProperty(_nightModePropertyName, "false")) ==
"true";
@ -173,7 +171,6 @@ Future<dynamic> initTheme() async {
themeEvent.broadcast();
});
}
}
_changeThemeByCode(await method.loadTheme());
}
@ -185,7 +182,7 @@ Future<dynamic> chooseTheme(BuildContext buildContext) async {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
var list = <SimpleDialogOption>[];
if (_androidVersion >= 29) {
if (androidVersion >= 29) {
var onChange = (bool? v) async {
if (v != null) {
await method.saveProperty(

View File

@ -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,12 +120,12 @@ class _DownloadExportToFileScreenState
}
List<Widget> _buildExportToFileButtons() {
List<Widget> widgets = [];
if (Platform.isWindows ||
Platform.isMacOS ||
Platform.isLinux ||
Platform.isAndroid) {
return [
MaterialButton(
widgets.add(MaterialButton(
onPressed: () async {
String? path = await chooseFolder(context);
print("path $path");
@ -150,9 +153,9 @@ class _DownloadExportToFileScreenState
}
},
child: _buildButtonInner('导出到HTML+JPG\n(可直接在相册中打开观看)'),
),
Container(height: 10),
MaterialButton(
));
widgets.add(Container(height: 10));
widgets.add(MaterialButton(
onPressed: () async {
String? path = await chooseFolder(context);
print("path $path");
@ -180,11 +183,55 @@ class _DownloadExportToFileScreenState
}
},
child: _buildButtonInner('导出到HTML.zip\n(可从其他设备导入 / 解压后可阅读)'),
),
Container(height: 10),
];
));
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<DownloadEp> 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) {

View File

@ -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<InitScreen> {
Future<dynamic> _init() async {
//
await initPlatform(); // ,
await autoClean();
await initAddress();
await initProxy();