mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
fix #310
This commit is contained in:
parent
eb8b7d31de
commit
72495b4c2a
12 changed files with 254 additions and 109 deletions
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Flutter",
|
|
||||||
"type": "dart",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "lib/main.dart",
|
|
||||||
"flutterMode": "profile"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,7 @@ android {
|
||||||
// compileSdk = flutter.compileSdkVersion
|
// compileSdk = flutter.compileSdkVersion
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
//ndkVersion = flutter.ndkVersion
|
//ndkVersion = flutter.ndkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "28.2.13676358"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
package eu.twonly
|
package eu.twonly
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
|
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
|
||||||
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
class MainActivity : FlutterFragmentActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
|
|
||||||
|
private val MEDIA_STORE_CHANNEL = "eu.twonly/mediaStore"
|
||||||
|
private lateinit var channel: MethodChannel
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
||||||
eventSink!!.success(true)
|
eventSink!!.success(true)
|
||||||
|
|
@ -18,4 +33,84 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
|
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MEDIA_STORE_CHANNEL)
|
||||||
|
|
||||||
|
channel.setMethodCallHandler {call, result ->
|
||||||
|
try {
|
||||||
|
if (call.method == "safeFileToDownload") {
|
||||||
|
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
|
||||||
|
val sourceFile = arguments["sourceFile"]
|
||||||
|
if (sourceFile == null) {
|
||||||
|
result.success(false)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
val context = applicationContext
|
||||||
|
val inputStream = FileInputStream(File(sourceFile))
|
||||||
|
|
||||||
|
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
|
||||||
|
|
||||||
|
val savedUri = saveZipToDownloads(context, outputName, inputStream)
|
||||||
|
if (savedUri != null) {
|
||||||
|
result.success(savedUri.toString())
|
||||||
|
} else {
|
||||||
|
result.error("SAVE_FAILED", "Could not save ZIP", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("EXCEPTION", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveZipToDownloads(
|
||||||
|
context: Context,
|
||||||
|
fileName: String = "archive.zip",
|
||||||
|
sourceStream: InputStream
|
||||||
|
): android.net.Uri? {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
} else {
|
||||||
|
MediaStore.Files.getContentUri("external")
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = resolver.insert(collection, contentValues) ?: return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolver.openOutputStream(uri).use { out: OutputStream? ->
|
||||||
|
requireNotNull(out) { "Unable to open output stream" }
|
||||||
|
sourceStream.use { input ->
|
||||||
|
input.copyTo(out)
|
||||||
|
}
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
|
||||||
|
resolver.update(uri, done, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package eu.twonly
|
||||||
|
|
||||||
|
class MyMediaStorageProxy {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,56 +1,56 @@
|
||||||
|
import CryptoKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import CryptoKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
if #available(iOS 10.0, *) {
|
|
||||||
//UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
|
|
||||||
}
|
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
|
||||||
|
|
||||||
// if (@available(iOS 10.0, *)) {
|
|
||||||
// [UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
|
|
||||||
// }
|
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
|
||||||
NSLog("Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@", response.notification.request.content.userInfo)
|
|
||||||
//...
|
|
||||||
}
|
|
||||||
|
|
||||||
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
NSLog("userNotificationCenter:willPresent")
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
/*
|
|
||||||
debugging NotificationService
|
override func userNotificationCenter(
|
||||||
let pushKeys = getPushKey();
|
_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
|
||||||
print(pushKeys)
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
|
) {
|
||||||
let bestAttemptContent = notification.request.content
|
NSLog(
|
||||||
|
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
|
||||||
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
response.notification.request.content.userInfo)
|
||||||
let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
|
//...
|
||||||
return completionHandler([.alert, .sound])
|
}
|
||||||
}
|
|
||||||
|
override func userNotificationCenter(
|
||||||
let data = getPushNotificationData(pushDataJson: push_data)
|
_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
|
||||||
print(data)
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
*/
|
) {
|
||||||
|
NSLog("userNotificationCenter:willPresent")
|
||||||
completionHandler([.alert, .sound])
|
|
||||||
|
/*
|
||||||
|
debugging NotificationService
|
||||||
|
let pushKeys = getPushKey();
|
||||||
|
print(pushKeys)
|
||||||
|
|
||||||
|
let bestAttemptContent = notification.request.content
|
||||||
|
|
||||||
|
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
||||||
|
let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
|
||||||
|
return completionHandler([.alert, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data = getPushNotificationData(pushDataJson: push_data)
|
||||||
|
print(data)
|
||||||
|
*/
|
||||||
|
|
||||||
|
completionHandler([.alert, .sound])
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,27 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
|
|
||||||
|
|
@ -80,17 +80,7 @@ void main() async {
|
||||||
unawaited(MediaFileService.purgeTempFolder());
|
unawaited(MediaFileService.purgeTempFolder());
|
||||||
|
|
||||||
await initFileDownloader();
|
await initFileDownloader();
|
||||||
if (Platform.isAndroid) {
|
unawaited(finishStartedPreprocessing());
|
||||||
if ((await DeviceInfoPlugin().androidInfo).version.release == '9') {
|
|
||||||
Future.delayed(const Duration(seconds: 20), () {
|
|
||||||
unawaited(finishStartedPreprocessing());
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
unawaited(finishStartedPreprocessing());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unawaited(finishStartedPreprocessing());
|
|
||||||
}
|
|
||||||
|
|
||||||
unawaited(createPushAvatars());
|
unawaited(createPushAvatars());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,19 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
|
||||||
media.ffmpegOutputPath.deleteSync();
|
media.ffmpegOutputPath.deleteSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var overLayCommand = '';
|
||||||
|
if (media.overlayImagePath.existsSync()) {
|
||||||
|
overLayCommand =
|
||||||
|
'-i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0"';
|
||||||
|
}
|
||||||
|
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
var command =
|
var command =
|
||||||
'-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"';
|
'-i "${media.originalPath.path}" $overLayCommand -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"';
|
||||||
|
|
||||||
if (media.removeAudio) {
|
if (media.removeAudio) {
|
||||||
command =
|
command =
|
||||||
'-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"';
|
'-i "${media.originalPath.path}" $overLayCommand -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"';
|
||||||
}
|
}
|
||||||
|
|
||||||
final session = await FFmpegKit.execute(command);
|
final session = await FFmpegKit.execute(command);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -64,36 +65,38 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
onChanged: (a) => toggleStoreInGallery(),
|
onChanged: (a) => toggleStoreInGallery(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
if (Platform.isAndroid)
|
||||||
title: Text(
|
ListTile(
|
||||||
context.lang.exportMemories,
|
title: Text(
|
||||||
|
context.lang.exportMemories,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) {
|
||||||
|
return const ExportMediaView();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () async {
|
if (Platform.isAndroid)
|
||||||
await Navigator.push(
|
ListTile(
|
||||||
context,
|
title: Text(
|
||||||
MaterialPageRoute(
|
context.lang.importMemories,
|
||||||
builder: (_) {
|
),
|
||||||
return const ExportMediaView();
|
onTap: () async {
|
||||||
},
|
await Navigator.push(
|
||||||
),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
},
|
builder: (_) {
|
||||||
),
|
return const ImportMediaView();
|
||||||
ListTile(
|
},
|
||||||
title: Text(
|
),
|
||||||
context.lang.importMemories,
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () async {
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) {
|
|
||||||
return const ImportMediaView();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,31 @@ import 'dart:io';
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
class AndroidMediaStore {
|
||||||
|
static const androidMediaStoreChannel = MethodChannel('eu.twonly/mediaStore');
|
||||||
|
|
||||||
|
static Future<bool> safeFileToDownload(File sourceFile) async {
|
||||||
|
try {
|
||||||
|
Log.info('Storing $sourceFile');
|
||||||
|
final storedPath = (
|
||||||
|
await androidMediaStoreChannel.invokeMethod('safeFileToDownload', {
|
||||||
|
'sourceFile': sourceFile.path,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
Log.info(storedPath);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ExportMediaView extends StatefulWidget {
|
class ExportMediaView extends StatefulWidget {
|
||||||
const ExportMediaView({super.key});
|
const ExportMediaView({super.key});
|
||||||
|
|
@ -20,6 +42,7 @@ class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
File? _zipFile;
|
File? _zipFile;
|
||||||
bool _isZipping = false;
|
bool _isZipping = false;
|
||||||
bool _zipSaved = false;
|
bool _zipSaved = false;
|
||||||
|
bool _isStoring = false;
|
||||||
|
|
||||||
Future<Directory> _mediaFolder() async {
|
Future<Directory> _mediaFolder() async {
|
||||||
final dir = MediaFileService.buildDirectoryPath(
|
final dir = MediaFileService.buildDirectoryPath(
|
||||||
|
|
@ -77,7 +100,6 @@ class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
_status = 'Adding $relative';
|
_status = 'Adding $relative';
|
||||||
});
|
});
|
||||||
|
|
||||||
// ZipFileEncoder doesn't give per-file progress; update after adding.
|
|
||||||
await encoder.addFile(f, relative);
|
await encoder.addFile(f, relative);
|
||||||
|
|
||||||
processedBytes += await f.length();
|
processedBytes += await f.length();
|
||||||
|
|
@ -108,17 +130,28 @@ class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
|
|
||||||
Future<void> _saveZip() async {
|
Future<void> _saveZip() async {
|
||||||
if (_zipFile == null) return;
|
if (_zipFile == null) return;
|
||||||
|
setState(() {
|
||||||
|
_isStoring = true;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
final outputFile = await FilePicker.platform.saveFile(
|
if (Platform.isAndroid) {
|
||||||
dialogTitle: 'Save your memories to desired location',
|
if (!await AndroidMediaStore.safeFileToDownload(_zipFile!)) {
|
||||||
fileName: p.basename(_zipFile!.path),
|
return;
|
||||||
bytes: _zipFile!.readAsBytesSync(),
|
}
|
||||||
);
|
} else {
|
||||||
if (outputFile == null) return;
|
final outputFile = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Save your memories to desired location',
|
||||||
|
fileName: p.basename(_zipFile!.path),
|
||||||
|
bytes: _zipFile!.readAsBytesSync(),
|
||||||
|
);
|
||||||
|
if (outputFile == null) return;
|
||||||
|
}
|
||||||
_zipSaved = true;
|
_zipSaved = true;
|
||||||
|
_isStoring = false;
|
||||||
_status = 'ZIP stored: ${p.basename(_zipFile!.path)}';
|
_status = 'ZIP stored: ${p.basename(_zipFile!.path)}';
|
||||||
setState(() {});
|
setState(() {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_isStoring = false;
|
||||||
setState(() => _status = 'Save failed: $e');
|
setState(() => _status = 'Save failed: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +198,9 @@ class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.save_alt),
|
icon: const Icon(Icons.save_alt),
|
||||||
label: const Text('Save ZIP'),
|
label: const Text('Save ZIP'),
|
||||||
onPressed: (_zipFile != null && !_isZipping) ? _saveZip : null,
|
onPressed: (_zipFile != null && !_isZipping && !_isStoring)
|
||||||
|
? _saveZip
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@ class _ImportMediaViewState extends State<ImportMediaView> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read zip bytes and decode
|
final stream = InputFileStream(zipFile.path);
|
||||||
final bytes = await zipFile.readAsBytes();
|
|
||||||
final archive = ZipDecoder().decodeBytes(bytes);
|
final archive = ZipDecoder().decodeStream(stream);
|
||||||
|
|
||||||
// Optionally: compute total entries to show progress
|
// Optionally: compute total entries to show progress
|
||||||
final entries = archive.where((e) => e.isFile).toList();
|
final entries = archive.where((e) => e.isFile).toList();
|
||||||
|
|
|
||||||
|
|
@ -1132,10 +1132,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1713,10 +1713,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.7"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue