ต่อจากเนื้อหาตอนที่แล้ว ที่เราได้รู้จักการใช้งาน Dio
ในกรณีใช้ในการดาวน์โหลดไฟล์จาก server ซึ่งเราสามารถ
ประยุกต์ใช้งานหลายๆ อย่าง เช่น แอปเราต้องการข้อมูลที่มีขนาดใหญ่
มาใช้งานในแอป เราก็ไม่จำเป็นต้องใส่ข้อมูลนั้นลงไปในแอปแต่แรก
เพราะจะทำให้มีขนาดใหญ่ แต่เราสามารถสร้างหน้าหรือส่่วนที่สำหรับ
โหลดข้อมูลเพิ่มเติมจาก server ลงมาใช้งานในแอปภายหลังได้ แบบนี้เป็นต้น
นั่นก็เป็นส่วนของเนื้อหาตอนที่แล้ว
สำหรับเนื้อหาตอนต่อไปนี้ ก็จะเป็นในอีกมุมหนึ่ง เช่น เรามีการใช้งานแอป มีการ
สร้างข้อมูลต่างๆ ขึ้น และต้องการเก็บไฟล์หรือข้อมูลนั้นๆ ไว้บนฝั่ง server เราก็สามารถใช้
Dio ตัวนี้ในการอัปโหลดไฟล์ขึ้นไปไว้บน server เพื่อเรียกใช้งานหรือสำรองข้อมูลได้ และ
มีวิธีการใช้งานที่ไม่ยุ่งยาก
การเตรียมใช้งาน Dio สามารถทบทวนได้ที่บทความตอนที่แล้ว
การดาวน์โหลดไฟล์ด้วย Dio ใน Flutter อย่างง่าย http://niik.in/1123
เนื่องจากเนื้อหาในตอนต่อไปนี้ เป็นการอัปโหลดไฟล์จากในแอปของเรา ดังนั้นเราจะใช้เนื้อหาจาก
บทความเกี่ยวกับการจัดการไฟล์ก่อนหน้า มาปรับใช้เล็กน้อย
การแชร์ไฟล์ Sharing files ด้วย Share Plus ใน Flutter http://niik.in/1119
ในบทความนั้นจะเป็นการแสดงรายการไฟล์ทั้งหมดใน app_flutter โฟลเดอร์ ซึ่งเราสามารถกด
เลือกไฟล์ต่างๆ ที่จะแชร์ได้ แต่เราจะเพิ่มส่วนของการกดเลือกไฟล์แล้ว สามารถที่จะอัปโหลดขึ้นไปยัง
server ตามตำแหน่งที่เราต้องการได้ โดยเมื่อทำการอัปโหลดก็จะแสดงสถานะเปอร์เช็นต์การอัปโหลด
เมื่ออัปโหลดเสร็จก็จะใช้ SnackBar แสดงสถานะการอัปโหลดว่า อัปโหลดได้กี่ไฟล์จากทั้งหมดกี่ไฟล์
ที่อัปโหลดไม่ได้ เพราะอะไรก็จะมีบอก
การเตรียมไฟล์อัปโหลดที่ฝั่ง Server
ในที่นี้เราจะใช้เป็น php สมมติเช่น server เราชื่อ htttps://example.com เราจะอัปโหลดไป
ไว้ที่โฟลเดอร์ชื่อ uploads จะใช้ชื่อไฟล์ว่า upload.php โครงสร้าง path ไฟล์ก็จะเป็น
// ไฟล์สำหรับอัปโหลดไฟล์ สามารถปรับแต่ง และกำหนดค่าต่างๆ ได้ htttps://example.com/upload.php // โฟลเดอร์สำหรับเก็บไฟล์ ตัวโปรแกรมจะสร้างรูปแบบ uploads/202410 // ปีเดือน แล้วก็จัดรูปแบบไฟล์ตามแสดงในโค้ด htttps://example.com/uploads/
ไฟล์ upload.php
<?php /** Error reporting ใช้ตอนพัฒนา ปิดเมื่อนำไปใช้งาน */ error_reporting(E_ALL); ini_set('display_errors', TRUE); ini_set('display_startup_errors', TRUE); // ตรวจสอบ Authorization ก่อนเริ่มการอัปโหลด ในที่นี้จะจำลอง สามารถไปปรับใช้เพิ่มเติมได้ // your_token = 123456 $headers = getallheaders(); $your_token = "123456"; if (!isset($headers['Authorization']) || $headers['Authorization'] != 'Bearer '.$your_token) { // กรณีไม่มีหรือไม่ตรงกับ token ที่ต้องการ header('Content-Type: application/json'); echo json_encode(array( 'uploaded' => false, 'error' => 'Unauthorized' )); exit; } // ฟังก์ชั่นแปลงขนาดไปหน่วน Byte เป็น MB function convertBytesToMB($bytes) { // Convert bytes to megabytes $megabytes = $bytes / (1024 * 1024); // Format the result to 2 decimal places return number_format($megabytes, 2); } // ฟังก์ชั่นแปลงขนาดไปหน่วน MB เป็น Byte function convertMBToBytes($megabytes) { // Convert megabytes to bytes return $megabytes * 1024 * 1024; } // ฟังก์ชั่นสำหรับตรวจสอบไม่ให้อัพโหลดทับไฟล์เดิม กรณีชื่อไฟล์ซ้ำ function getUniqueFileName($directory, $filename) { // ถ้ายังไม่มีชื่อไฟล์นี้อยู่ ก็ให้ให้ใช้ชื่อไฟล์เริ่มต้น if (!file_exists($directory . $filename)) { return $filename; } // กรณีมีชื่อไฟล์นี้อยู่แล้ว ตรวจสอบข้อมูลไฟล์ $extension = pathinfo($filename, PATHINFO_EXTENSION); $basename = pathinfo($filename, PATHINFO_FILENAME); // สร้างตัวแปรนับจำนวนไฟล์ที่ชื่อซ้ำ เพื่อใช้เป็นตัวเลข ต่อไปในชื่อไฟล์นั้นๆ $counter = 1; // วนลูปไปจนกว่าจะไม่ซ้ำกับไฟล์ใดๆก่อนหน้า while (file_exists($directory . $basename . '_' . $counter . '.' . $extension)) { $counter++; } // ส่งกลับชื่อไฟล์ที่ไม่ซ้ำกลับไปใช้งาน return $basename . '_' . $counter . '.' . $extension; } // ตรวจสอบว่ามีการเลือกไฟล์เพื่ออัพโหลดหรือไม่ if (isset($_FILES['uploads'])) { $response = array('uploaded_files' => array()); // เก็บผลลัพธ์ไฟล์ที่อัปโหลดสำเร็จ // วนลูปจัดการไฟล์แต่ละไฟล์ที่อัปโหลด foreach ($_FILES['uploads']['name'] as $key => $file_name) { if ($_FILES['uploads']['name'][$key] != "" ) { // มีชื่อไฟล์ที่อัปโหลด $file_tmp = $_FILES['uploads']['tmp_name'][$key]; // ข้อมูลไฟล์ $file_size = $_FILES['uploads']['size'][$key]; // ขนาดไฟล์ // กำหนดโฟลเดอร์ สำหรับเก็บไฟล์ $upload_dir = "uploads/"; // กรณีกำหนดขนาดไฟล์ไม่่เกิน $max_file_size = 2; // 2 MB // กรณีกำหนดเงื่อนไขว่าต้องเป็นไฟล์นามสกุล ที่กำหนดเท่านั้น $check_allow_format = false; // true or false $allowedFormats = array("jpg", "jpeg", "png", "gif"); // กำหนดตรวจสอบว่าต้องเป็นเฉพาะรูปหรือไม่ $check_image_only = false; // true or false // กำหนดตัวแปร กรณ๊เกิดข้อผิดพลาด $error_message = ''; $uploadOk = 1; // ตรวจสอบข้อมูลไฟล์ว่าเป็นรูปภาพหรือไม่ if($check_image_only){ $check = getimagesize($file_tmp); if ($check !== false) { $uploadOk = 1; } else { $error_message = "File is not an image."; $uploadOk = 0; } } // กรณีกำหนดเงื่อนไขว่าต้องเป็นไฟล์นามสกุล ที่กำหนดเท่านั้น if($check_allow_format){ $fileExtension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); if (!in_array($fileExtension, $allowedFormats)) { $error_message = "Sorry, only ".implode(",",$allowedFormats)." files are allowed."; $uploadOk = 0; } } // กรณีกำหนดขนาดไฟล์ไม่่เกินขนาดสูงสุดที่กำหนด if ($file_size > convertMBToBytes($max_file_size)) { $error_message = "Sorry, your file is too large. " .convertBytesToMB($file_size)." MB Limit ".$max_file_size." MB"; $uploadOk = 0; } // กรณีไม่ผ่านเงื่อนไขการตรวจสอบไฟล์ก่อนอัปโหลด if ($uploadOk == 0) { $response['uploaded_files'][] = array( 'file_name' => $file_name, 'uploaded' => false, 'error' => $error_message ); } else { // กรณีเงื่อนไขผ่าน พร้อมอัปโหลด // สร้างตัวแปรวันที่สำหรับ ใช้สร้างโฟลเดอร์ย่อย แยกตามเดือนและปี ณ ขณะนั้น $currentYear = date("Y"); $currentMonth = date("m"); // จัดรูปแบบ ปีเดือน เช่น 202401 เป็นโฟลเดอร์ข้อมูล ปี 2024 เดือน 01 รูปทั้งหมด // ที่อัปโหลดช่วงนี้ จะถูกจัดเก็บไว้ในนี้ $folderName = $currentYear . sprintf("%02d", $currentMonth); // ตรวจสอบว่ามีโฟลเดอร์ดังกล่าว อยู่แล้วหรือไม่ ถ้าไม่มีให้ทำการสร้างขึ้นมา if (!file_exists($upload_dir . $folderName)) { mkdir($upload_dir . $folderName, 0777, true); $upload_dir = $upload_dir . $folderName . "/"; } else { $upload_dir = $upload_dir . $folderName . "/"; } // นำชื่อไฟล์ไปตรวจสอบ ว่ามีชื่อไฟล์นี้อยู่แล้วหรือไม่ ถ้ามีซ้า ก็จะทำการเพิ่มตัวเลขต่อท้ายเข้าไป $file_name = getUniqueFileName($upload_dir, $file_name); // ทำการย้ายข้อมูลไฟล์ที่อัปโหลดไปไว้ในโฟลเดอร์ และ ชื่อไฟล์ที่กำหนด if (move_uploaded_file($file_tmp, $upload_dir . $file_name)) { // ส่งกลับข้อมูลที่อยู่หรือ url ของไฟล์รูปภาพที่อัปโหลด พร้อมสถานะการอัปโหลดที่สำเร็จ $file_url = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['REQUEST_URI']) . '/' . $upload_dir . $file_name; $response['uploaded_files'][] = array( 'file_name' => $file_name, 'uploaded' => true, 'url' => $file_url ); } else { // เกิดข้อผิดพลาดไม่สามารถย้ายข้อมูลไฟล์ หรืออัปโหลดไฟล์ได้ เช่น ถูกปิด permission $response['uploaded_files'][] = array( 'file_name' => $file_name, 'uploaded' => false, 'error' => 'Failed to move uploaded file. '.$file_size ); } } } else { // ไม่มีการเลือกไปล์เพื่อผัปโหลด $response['uploaded_files'][] = array( 'file_name' => $file_name, 'uploaded' => false, 'error' => 'No file uploaded.' ); } } // คืนค่าผลลัพธ์การอัปโหลดทั้งหมดเป็น JSON header('Content-Type: application/json'); echo json_encode($response); exit; } else { // ไม่มีการเลือกไฟล์เพื่ออัปโหลด $response = array( 'uploaded' => false, 'error' => 'No files uploaded.' ); header('Content-Type: application/json'); echo json_encode($response); exit; }
โค้ดนี้เป็นการปรับแต่งมาจากบทความ
การใช้ CKEditor 5 แบบ CDN รองรับการอัปโหลดไฟล์ อย่างง่าย http://niik.in/1077
การอัปโหลดไฟล์ขึ้น Server ด้วย Dio
เมื่อเราได้ส่วนของระบบด้านหลังในการอัปโหลดไฟล์แล้ว เราก็กลับมาในส่วนของแอปเรา จะขอแยก
ส่วนอธิบาย ก่อนรวมไว้ในไฟล์เดียวกันดังนี้
ส่วนสำหรับการเก็บความก้าวหน้าของการอัปโหลด หรือเปอร์เซ็นต์การอัปโหลด และส่วนที่บอกว่า
สถานะตอนนี้กำลังอัปโหลดอยู่หรือไม่
// กำกนดตัวเก็บค่าสถานะการโหลดข้อมูล final ValueNotifier<double> _progressNotifier = ValueNotifier<double>(0); bool is_uploading = false; // สถานะกำลังอัปโหลด
ต่อเป็นส่วนของฟังก์ชั่นสำหรับการอัปโหลด
// ฟังก์ชั่นสำหรับอัปโหลดไฟล์ขึ้น server void uploadFile() async { Dio dio = Dio(); // กำหนดรายการไฟล์ที่จะอัปโหลด List<File> _fileUpload = <File>[]; // วนลูปจากรายการที่เลือก _selectedItems.forEach((index) async { _fileUpload.add(File(_files![index].path)); }); try { // เตรียมข้อมูลไฟล์รองรับหลายไฟล์ tFormData with multiple files FormData formData = FormData.fromMap({ 'uploads[]': [ for (var file in _fileUpload) await MultipartFile.fromFile(file.path) ] }); // ส่ง request อัปโหลดไฟล์ไปยังเซิร์ฟเวอร์ Response response = await dio.post( // เปลี่ยน URL เป็น endpoint ของเซิร์ฟเวอร์ที่ต้องการอัปโหลดไฟล์ 'https://example.com/upload.php', data: formData, options: Options( headers: { // ถ้าจำเป็นต้องมี Authorization หรือ headers เพิ่มเติม จำลองใช้ตัวเลข 123456 'Authorization': 'Bearer 123456', }, ), // ฟังก์ชันเพื่อติดตามความก้าวหน้าของการอัปโหลด onSendProgress: (int sent, int total) { _progressNotifier.value = (sent / total).toDouble(); // print(_progressNotifier.value); print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%'); }, ); // ตรวจสอบการตอบกลับจากเซิร์ฟเวอร์ อาจจะอัปโหลดได้หรือไม่ ก็ป็นได้ แต่มีการส่งค่ากลับมา if (response.statusCode == 200) { print(response.data); // ตัวอย่างข้อมูลที่ส่งกลับมา print(response.data['uploaded_files'].runtimeType); // ดูชนิดประเภทของข้อมูล // ถ้ากระบวนการอัปโหลดเรียบร้อยแล้ว และมีการส่งค่าตอบกลับมา if(_progressNotifier.value==1 && response.data['uploaded_files'].isNotEmpty){ setState(() { // นำค่าข้อมูลที่ตอบกลับมาไปจัดรูปแบบการแสดงด้วยฟังก์ชั่น _showUploadResultSnackbar(response.data['uploaded_files']); _progressNotifier.value = 0.0; is_uploading = false; }); } } else { // กรณี server ไม่ตอบกลับโค้ดฝั่ง server อาจจะมีปัญหา setState(() { _progressNotifier.value = 0.0; is_uploading = false; }); // เกิดข้อผิดพลาด อัปโหลดไฟล์ไม่สำเร็จ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to upload file")), ); } } catch (e) { // เมื่อไม่สามารถอัปโหลดไฟล์ได้ อาจจะเกิดจากฝั่ง server เช่น ไม่อนุญาต ขนาดไฟล์เกิน หรืออื่นๆ setState(() { _progressNotifier.value = 0.0; is_uploading = false; }); // เกิดข้อผิดพลาด อัปโหลดไฟล์ไม่สำเร็จ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error uploading file: $e")), ); } }
ต่อไปเป็นส่วนของการปรับแต่งการใช้งาน SnackBar เพื่อแจ้งสถานะการอัปโหลดไฟล์ ฟังก์ชั่น
นี้ถูกเรียกใช้งานในฟังก์ชั่นอัปโหลดด้านบนอีกที
// ส่วนของฟังก์ชั่นแสดงการแจ้งเตือนรายการที่อัปโหลด void _showUploadResultSnackbar(List<dynamic> fileUploads) { // นับส่วนที่อัปโหลดสำเร็จ 'uploaded' is true int uploadedCount = fileUploads.where((file) => file['uploaded'] == true).length; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( height: 200, // ปรับขนาดความสูงของการแสดง child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Upload Status (${uploadedCount}/${fileUploads.length})', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), Expanded( child: ListView.builder( itemCount: fileUploads.length, itemBuilder: (context, index) { final result = fileUploads[index]; return ListTile( leading: result['uploaded'] ? Icon(Icons.check, color: Colors.green) : Icon(Icons.error, color: Colors.red), title: Text(result['file_name'], style: TextStyle( color: Colors.white, fontSize: 15, ),), subtitle: Text(result['uploaded'] ? 'Upload successful' : 'Upload failed: ${result['error']}', style: TextStyle( color: Colors.white, fontSize: 13, ),), ); }, ), ), ], ), ), // duration: Duration(seconds: 100), // กำหนดแสดงชั่วคราว duration: const Duration(days: 365), // กำหนดแสดงแบบถาวร behavior: SnackBarBehavior.floating, // floating / fixed backgroundColor: Colors.grey[850], action: SnackBarAction( label: 'Close', textColor: Colors.white, onPressed: () { // เมื่อกดปิด ScaffoldMessenger.of(context).hideCurrentSnackBar(); // Dismisses the SnackBar }, ), ), ); }
และสุดท้ายก็เป็นส่วนของปุ่มจัดการ และสถานะเปอร์เซ็นต์การอัปโหลด ที่เราแสดงไว้ตรงสว่น action
เมนูใน appBar จะมีด้วยกันสองส่วนคือ ส่วนแสดงสถานะเปอร์เซ็นต์การอัปโหลด กับ ส่วนของปุ่มในการ
อัปโหลด โดยสถานะจะแสดงเมื่อกำลังอัปโหลดไฟล์ ส่วนปุ่มจะแสดงเมื่อมีการเลือกไฟล์ และยังไม่อัปโหลด
ไฟล์ นั่นก็คือ ถ้ากดเลือกไฟล์ ก็จะแสดงปุ่ม แล้วถ้ากดที่ปุ่ม ก็จะแสดงสถานะ ปุ่มก็จะหายไป จะกลับมา
แสดงอีกครั้งก็ตอนอัปโหลดเสร็จเรียบร้อยแล้วหรือสิ้นสุดกระบวนการอัปโหลด
// ส่วนนี้แสดงใน action ของ appBar if (uploadProgress > 0) { return Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: uploadProgress, backgroundColor: Colors.grey, valueColor: AlwaysStoppedAnimation<Color>(Colors.green), ), Text( "${(uploadProgress * 100).toInt()}%", // Display the progress percentage style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black, ), ), ], ); } else { return SizedBox.shrink(); // Hide when there's no progress } }, ), if (_selectedItems.isNotEmpty && !is_uploading) IconButton( onPressed: () async { print("xdebug: upload file"); // เรียกใช้งาน setState(() { is_uploading = true; uploadFile(); }); }, icon: FaIcon(FontAwesomeIcons.upload), ),
ไฟล์ด้าานล่างจะรวมทุกส่วนไว้ด้วยกัน เป็นส่วนหนึ่งของไฟล์จากบทความการแชร์ไฟล์ด้วย Shared Plus ซึ่งท้ายบทความนั้น
มีโค้ดทั้งหมดให้ดาวน์โหลด หรือถ้าต้องการประยุกต์เอง ก็ดูเฉพาะส่วนด้านบน เป็นแนวทางได้เลย
ไฟล์ mediaexplorer.dart
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:dio/dio.dart'; import 'audioplayer.dart'; import 'fullscreenvideo.dart'; import 'viewphotoscreen.dart'; class MediaExplorer extends StatefulWidget { static const routeName = '/gallery'; const MediaExplorer({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _MediaExplorerState(); } } class _MediaExplorerState extends State<MediaExplorer> { List<FileSystemEntity>? _files; List<int> _selectedItems = []; // ignore: unused_field String _currentPath = ''; // กำกนดตัวเก็บค่าสถานะการโหลดข้อมูล final ValueNotifier<double> _progressNotifier = ValueNotifier<double>(0); bool is_uploading = false; // สถานะกำลังอัปโหลด @override void initState() { super.initState(); _requestPermissionsAndLoadMedia(); } // ฟังก์ชั่นสำหรับอัปโหลดไฟล์ขึ้น server void uploadFile() async { Dio dio = Dio(); // กำหนดรายการไฟล์ที่จะอัปโหลด List<File> _fileUpload = <File>[]; // วนลูปจากรายการที่เลือก _selectedItems.forEach((index) async { _fileUpload.add(File(_files![index].path)); }); try { // เตรียมข้อมูลไฟล์รองรับหลายไฟล์ tFormData with multiple files FormData formData = FormData.fromMap({ 'uploads[]': [ for (var file in _fileUpload) await MultipartFile.fromFile(file.path) ] }); // ส่ง request อัปโหลดไฟล์ไปยังเซิร์ฟเวอร์ Response response = await dio.post( // เปลี่ยน URL เป็น endpoint ของเซิร์ฟเวอร์ที่ต้องการอัปโหลดไฟล์ 'https://example.com/upload.php', data: formData, options: Options( headers: { // ถ้าจำเป็นต้องมี Authorization หรือ headers เพิ่มเติม จำลองใช้ตัวเลข 123456 'Authorization': 'Bearer 123456', }, ), // ฟังก์ชันเพื่อติดตามความก้าวหน้าของการอัปโหลด onSendProgress: (int sent, int total) { _progressNotifier.value = (sent / total).toDouble(); // print(_progressNotifier.value); print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%'); }, ); // ตรวจสอบการตอบกลับจากเซิร์ฟเวอร์ อาจจะอัปโหลดได้หรือไม่ ก็ป็นได้ แต่มีการส่งค่ากลับมา if (response.statusCode == 200) { print(response.data); // ตัวอย่างข้อมูลที่ส่งกลับมา print(response.data['uploaded_files'].runtimeType); // ดูชนิดประเภทของข้อมูล // ถ้ากระบวนการอัปโหลดเรียบร้อยแล้ว และมีการส่งค่าตอบกลับมา if(_progressNotifier.value==1 && response.data['uploaded_files'].isNotEmpty){ setState(() { // นำค่าข้อมูลที่ตอบกลับมาไปจัดรูปแบบการแสดงด้วยฟังก์ชั่น _showUploadResultSnackbar(response.data['uploaded_files']); _progressNotifier.value = 0.0; is_uploading = false; }); } } else { // กรณี server ไม่ตอบกลับโค้ดฝั่ง server อาจจะมีปัญหา setState(() { _progressNotifier.value = 0.0; is_uploading = false; }); // เกิดข้อผิดพลาด อัปโหลดไฟล์ไม่สำเร็จ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to upload file")), ); } } catch (e) { // เมื่อไม่สามารถอัปโหลดไฟล์ได้ อาจจะเกิดจากฝั่ง server เช่น ไม่อนุญาต ขนาดไฟล์เกิน หรืออื่นๆ setState(() { _progressNotifier.value = 0.0; is_uploading = false; }); // เกิดข้อผิดพลาด อัปโหลดไฟล์ไม่สำเร็จ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error uploading file: $e")), ); } } // ส่วนของฟังก์ชั่นแสดงการแจ้งเตือนรายการที่อัปโหลด void _showUploadResultSnackbar(List<dynamic> fileUploads) { // นับส่วนที่อัปโหลดสำเร็จ 'uploaded' is true int uploadedCount = fileUploads.where((file) => file['uploaded'] == true).length; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( height: 200, // ปรับขนาดความสูงของการแสดง child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Upload Status (${uploadedCount}/${fileUploads.length})', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), Expanded( child: ListView.builder( itemCount: fileUploads.length, itemBuilder: (context, index) { final result = fileUploads[index]; return ListTile( leading: result['uploaded'] ? Icon(Icons.check, color: Colors.green) : Icon(Icons.error, color: Colors.red), title: Text(result['file_name'], style: TextStyle( color: Colors.white, fontSize: 15, ),), subtitle: Text(result['uploaded'] ? 'Upload successful' : 'Upload failed: ${result['error']}', style: TextStyle( color: Colors.white, fontSize: 13, ),), ); }, ), ), ], ), ), // duration: Duration(seconds: 100), // กำหนดแสดงชั่วคราว duration: const Duration(days: 365), // กำหนดแสดงแบบถาวร behavior: SnackBarBehavior.floating, // floating / fixed backgroundColor: Colors.grey[850], action: SnackBarAction( label: 'Close', textColor: Colors.white, onPressed: () { // เมื่อกดปิด ScaffoldMessenger.of(context).hideCurrentSnackBar(); // Dismisses the SnackBar }, ), ), ); } // ฟังก์ชันสำหรับขอสิทธิ์และโหลดไฟล์ void _requestPermissionsAndLoadMedia() async { // ขอสิทธิ์การเข้าถึงไฟล์สื่อ PermissionStatus status = await Permission.storage.request(); if (status.isGranted) { // ถ้าได้รับสิทธิ์แล้วให้โหลดไฟล์ _loadMediaFiles(); } else { // กรณีไม่ได้รับสิทธิ์ print("Permission Denied"); } } void _loadMediaFiles() async { Directory appDir = await getApplicationDocumentsDirectory(); String folderPath = '${appDir.path}'; // เปลี่ยนเป็นโฟลเดอร์ที่ต้องการ Directory folderDir = Directory(folderPath); setState(() { _currentPath = folderDir.path; _files = folderDir.listSync() .where((file) => file is File) .toList(); }); } Widget _buildIcon(FileSystemEntity file) { String ext = file.path.split('.').last.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif'].contains(ext)) { return FaIcon(FontAwesomeIcons.image, color: Colors.blue); } else if (['mp3', 'wav'].contains(ext)) { return FaIcon(FontAwesomeIcons.music, color: Colors.green); } else if (['mp4', 'avi'].contains(ext)) { return FaIcon(FontAwesomeIcons.video, color: Colors.red); } else { return FaIcon(FontAwesomeIcons.file, color: Colors.grey); } } void _onLongPress(int index) { setState(() { if (_selectedItems.contains(index)) { _selectedItems.remove(index); } else { _selectedItems.add(index); } }); } // การยกเลิกและล้างค่าต่างๆ เมื่อได้ใช้งาน เพื่อคืนหน่วยความจำ @override void dispose() { _progressNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Media Explorer'), actions: [ ValueListenableBuilder<double>( valueListenable: _progressNotifier, builder: (context, uploadProgress, child) { if (uploadProgress > 0) { return Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: uploadProgress, backgroundColor: Colors.grey, valueColor: AlwaysStoppedAnimation<Color>(Colors.green), ), Text( "${(uploadProgress * 100).toInt()}%", // Display the progress percentage style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black, ), ), ], ); } else { return SizedBox.shrink(); // Hide when there's no progress } }, ), if (_selectedItems.isNotEmpty && !is_uploading) IconButton( onPressed: () async { print("xdebug: upload file"); // เรียกใช้งาน setState(() { is_uploading = true; uploadFile(); }); }, icon: FaIcon(FontAwesomeIcons.upload), ), if (_selectedItems.isNotEmpty) IconButton( onPressed: () async { print("xdebug: share"); try { List<XFile> _fileShare = <XFile>[]; _selectedItems.forEach((index) async { _fileShare.add(XFile(_files![index].path)); }); if(_fileShare.isNotEmpty){ final result = await Share.shareXFiles(_fileShare); if (result.status == ShareResultStatus.success) { print('xdebug: Thank you for sharing!'); } } } catch (e) { print(e); } }, icon: FaIcon(FontAwesomeIcons.share), ), if(_selectedItems.isNotEmpty) IconButton( icon: FaIcon(FontAwesomeIcons.trashCan), onPressed: () async { bool _confirm; // สร้างตัวแปรรับค่า ยืนยันการลบ _confirm = await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("Confirm"), content: const Text("Are you sure you wish to delete selected item?"), actions: <Widget>[ ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text("DELETE")), ElevatedButton( onPressed: () => Navigator.of(context).pop(false), child: const Text("CANCEL"), ), ], ); }, ); if (_confirm) { // ถ้ายืนยันการลบ เป็น true try { // โหลดข้อมูลใหม่อีกครั้ง setState(() { _selectedItems.forEach((index) { File file = _files![index] as File; file.deleteSync(); }); _selectedItems.clear(); _loadMediaFiles(); // Reload after deletion }); } catch (e) { print(e); } } }, ), ] ), body: _files != null ? GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 4.0, mainAxisSpacing: 4.0, ), itemCount: _files?.length ?? 0, itemBuilder: (context, index) { FileSystemEntity file = _files![index]; bool isSelected = _selectedItems.contains(index); String ext = file.path.split('.').last.toLowerCase(); return GestureDetector( onTap: (){ // ถ้ามีการเลือกไฟล์ if(_selectedItems.isNotEmpty){ _onLongPress(index); }else{ // ถ้าเป็นการกดเปิดปกติ if (['jpg', 'jpeg', 'png', 'gif'].contains(ext)) { Navigator.push( context, MaterialPageRoute( builder: (context) => ViewPhotoScreen( photos: file.path,), ), ); } else if (['mp3', 'wav'].contains(ext)) { Navigator.push( context, MaterialPageRoute( builder: (context) => AudioPlayers( fileName: file.path, ), ), ); } else if (['mp4', 'avi'].contains(ext)) { Navigator.push( context, MaterialPageRoute( builder: (context) => FullScreenVideo( file.path), ), ); } else { } } }, onLongPress: () => _onLongPress(index), child: GridTile( footer: GridTileBar( backgroundColor: isSelected ? Colors.blue.withOpacity(0.5) : Colors.black54, title: Text( file.path.split('/').last, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12), ), ), child: Container( color: isSelected ? Colors.blue.withOpacity(0.3) : Colors.white, child: Center(child: _buildIcon(file)), ), ), ); }, ) : Center(child: CircularProgressIndicator()), ); } }
ผลลัพธ์ที่ได้
เมื่อเลือกไฟล์ที่จะอัปโหลด ก็จะแสดงปุ่ม อัปโหลด ข้างปุ่มแชร์ เมื่อกดปุ่ม อัปโหลด ก็จะซ่อนปุ่มนี้ แล้ว
แสดงเป็นเปอร์เซ็นต์สถานะที่กำลังอัปโหลดแทน เมื่ออัปโหลดสำเร็จ ก็จะแจ้งสถานะว่าอัปโหลดผ่านกี่
ไฟล์ จากทั้งหมดกี่ไฟล์ พร้อมระบุสาเหตุว่าไฟล์นั้นๆ ที่อัปโหลดไม่ผ่านเพราะอะไร จริงๆ แล้วเราสามารถ
ระบุ url ของ path ไฟล์บน server กรณีอัปโหลดสำเร็จได้ แต่ในที่นี้ไม่ได้เรียกใช้งาน เราสามารถ
อัปโหลด 1 ไฟล์ หรือหลายไฟล์ก็ได้ ตัวอย่างนี้รองรับทั้งสองแบบ
เป็นอันเรียบร้อยในส่วนของการทำงานหลักๆ ของ Dio ทั้งที่ใช้ในการดาวน์โหลด ตามตัวอย่างตอน
ที่แล้ว และใช้ในการอัปโหลดในตัวอย่างตอนนี้ หวังว่าจะเป็นแนวทางไปปรับประยุกต์ใช้งานต่อไป
สำหรับเนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม