Kaynağa Gözat

feat: export download uses system file picker to choose save location

nonvioce 3 ay önce
ebeveyn
işleme
9d1760e3f6

+ 52 - 14
tiku-client/android/app/src/main/kotlin/com/tiku/tiku_client/MainActivity.kt

@@ -12,24 +12,45 @@ import java.io.FileOutputStream
 class MainActivity : FlutterActivity() {
 class MainActivity : FlutterActivity() {
     private val CHANNEL = "com.tiku/file_picker"
     private val CHANNEL = "com.tiku/file_picker"
     private var pendingResult: MethodChannel.Result? = null
     private var pendingResult: MethodChannel.Result? = null
+    private var saveFileData: ByteArray? = null
+    private var saveFileName: String? = null
 
 
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
         super.configureFlutterEngine(flutterEngine)
         super.configureFlutterEngine(flutterEngine)
         MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
         MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
             .setMethodCallHandler { call, result ->
             .setMethodCallHandler { call, result ->
-                if (call.method == "pickFile") {
-                    pendingResult = result
-                    val mimeTypes = arrayOf(
-                        "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
-                    )
-                    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
-                        addCategory(Intent.CATEGORY_OPENABLE)
-                        type = "*/*"
-                        putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
+                when (call.method) {
+                    "pickFile" -> {
+                        pendingResult = result
+                        val mimeTypes = arrayOf(
+                            "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+                        )
+                        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+                            addCategory(Intent.CATEGORY_OPENABLE)
+                            type = "*/*"
+                            putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
+                        }
+                        startActivityForResult(intent, FILE_PICK_REQUEST)
+                    }
+                    "saveFile" -> {
+                        val filePath = call.argument<String>("filePath") ?: ""
+                        val fileName = call.argument<String>("fileName") ?: "export.docx"
+                        val file = File(filePath)
+                        if (file.exists()) {
+                            saveFileData = file.readBytes()
+                            saveFileName = fileName
+                            pendingResult = result
+                            val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+                                addCategory(Intent.CATEGORY_OPENABLE)
+                                type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+                                putExtra(Intent.EXTRA_TITLE, fileName)
+                            }
+                            startActivityForResult(intent, SAVE_FILE_REQUEST)
+                        } else {
+                            result.error("FILE_NOT_FOUND", "File not found: $filePath", null)
+                        }
                     }
                     }
-                    startActivityForResult(intent, FILE_PICK_REQUEST)
-                } else {
-                    result.notImplemented()
+                    else -> result.notImplemented()
                 }
                 }
             }
             }
     }
     }
@@ -39,11 +60,9 @@ class MainActivity : FlutterActivity() {
         if (requestCode == FILE_PICK_REQUEST) {
         if (requestCode == FILE_PICK_REQUEST) {
             val result = pendingResult ?: return
             val result = pendingResult ?: return
             pendingResult = null
             pendingResult = null
-
             if (resultCode == RESULT_OK && data?.data != null) {
             if (resultCode == RESULT_OK && data?.data != null) {
                 val uri = data.data!!
                 val uri = data.data!!
                 try {
                 try {
-                    // Copy content URI to a temp file so Dart can read it
                     val inputStream = contentResolver.openInputStream(uri)
                     val inputStream = contentResolver.openInputStream(uri)
                     val fileName = getFileName(uri)
                     val fileName = getFileName(uri)
                     val tempFile = File(cacheDir, fileName ?: "upload.docx")
                     val tempFile = File(cacheDir, fileName ?: "upload.docx")
@@ -59,6 +78,24 @@ class MainActivity : FlutterActivity() {
             } else {
             } else {
                 result.success(null)
                 result.success(null)
             }
             }
+        } else if (requestCode == SAVE_FILE_REQUEST) {
+            val result = pendingResult ?: return
+            pendingResult = null
+            if (resultCode == RESULT_OK && data?.data != null && saveFileData != null) {
+                try {
+                    val uri = data.data!!
+                    contentResolver.openOutputStream(uri)?.use { output ->
+                        output.write(saveFileData)
+                    }
+                    result.success(true)
+                } catch (e: Exception) {
+                    result.error("SAVE_ERROR", "Failed to save: ${e.message}", null)
+                }
+                saveFileData = null
+                saveFileName = null
+            } else {
+                result.success(false) // user cancelled
+            }
         }
         }
     }
     }
 
 
@@ -75,5 +112,6 @@ class MainActivity : FlutterActivity() {
 
 
     companion object {
     companion object {
         private const val FILE_PICK_REQUEST = 1001
         private const val FILE_PICK_REQUEST = 1001
+        private const val SAVE_FILE_REQUEST = 1002
     }
     }
 }
 }

+ 17 - 3
tiku-client/lib/pages/export/export_page.dart

@@ -1,9 +1,12 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import '../../services/api_service.dart';
 import '../../services/api_service.dart';
 import '../../providers/app_provider.dart';
 import '../../providers/app_provider.dart';
 
 
+const _channel = MethodChannel('com.tiku/file_picker');
+
 class ExportPage extends ConsumerStatefulWidget {
 class ExportPage extends ConsumerStatefulWidget {
   final int? cartId;
   final int? cartId;
   final String? cartName;
   final String? cartName;
@@ -42,12 +45,23 @@ class _ExportPageState extends ConsumerState<ExportPage> {
   Future<void> _downloadFile(String filename, String label) async {
   Future<void> _downloadFile(String filename, String label) async {
     final api = ref.read(apiServiceProvider);
     final api = ref.read(apiServiceProvider);
     try {
     try {
+      // Step 1: download to cache
       final dir = await getApplicationDocumentsDirectory();
       final dir = await getApplicationDocumentsDirectory();
-      final savePath = '${dir.path}/$filename';
-      await api.downloadExport(filename, savePath);
+      final tmpPath = '${dir.path}/$filename';
+      await api.downloadExport(filename, tmpPath);
+
+      // Step 2: let user choose save location
+      final saved = await _channel.invokeMethod<bool>('saveFile', {
+        'filePath': tmpPath,
+        'fileName': filename,
+      });
+
       if (mounted) {
       if (mounted) {
         ScaffoldMessenger.of(context).showSnackBar(
         ScaffoldMessenger.of(context).showSnackBar(
-          SnackBar(content: Text('$label 已下载'), duration: const Duration(seconds: 2)),
+          SnackBar(
+            content: Text(saved == true ? '$label 已保存' : '已取消'),
+            duration: const Duration(seconds: 2),
+          ),
         );
         );
       }
       }
     } catch (e) {
     } catch (e) {