device_storage 1.0.0
device_storage: ^1.0.0 copied to clipboard
A Flutter plugin for saving, retrieving, and managing files in device storage including root directory and DCIM folder. Supports images, videos, audio, and all file types with easy-to-use API.
example/lib/main.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:device_storage/device_storage.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Device Storage Manager',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const StorageManagerScreen(),
);
}
}
class StorageManagerScreen extends StatefulWidget {
const StorageManagerScreen({super.key});
@override
State<StorageManagerScreen> createState() => _StorageManagerScreenState();
}
class _StorageManagerScreenState extends State<StorageManagerScreen> {
final DeviceStorage _storage = DeviceStorage();
final ImagePicker _picker = ImagePicker();
File? _selectedFile;
bool _isLoading = false;
String _statusMessage = '';
List<String> _fileList = [];
Uint8List? _retrievedFileBytes;
// Storage options
bool _saveToRoot = true;
String _folderPath = 'PicON';
final TextEditingController _folderController = TextEditingController(
text: 'PicON',
);
final TextEditingController _fileNameController = TextEditingController();
@override
void initState() {
super.initState();
_checkPermissions();
_folderController.addListener(() {
setState(() {
_folderPath = _folderController.text;
});
});
}
@override
void dispose() {
_folderController.dispose();
_fileNameController.dispose();
super.dispose();
}
Future<void> _checkPermissions() async {
final hasPermission = await _storage.hasPermissions();
if (!hasPermission) {
await _storage.requestPermissions();
}
}
void _showMessage(String message, {bool isError = false}) {
setState(() {
_statusMessage = message;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
duration: const Duration(seconds: 3),
),
);
}
Future<void> _pickImage() async {
try {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_selectedFile = File(image.path);
_fileNameController.text = image.name;
_statusMessage = 'Image selected: ${image.name}';
});
}
} catch (e) {
_showMessage('Error picking image: $e', isError: true);
}
}
Future<void> _captureImage() async {
try {
final XFile? image = await _picker.pickImage(source: ImageSource.camera);
if (image != null) {
setState(() {
_selectedFile = File(image.path);
_fileNameController.text = image.name;
_statusMessage = 'Photo captured';
});
}
} catch (e) {
_showMessage('Error capturing photo: $e', isError: true);
}
}
Future<void> _pickVideo() async {
try {
final XFile? video = await _picker.pickVideo(source: ImageSource.gallery);
if (video != null) {
setState(() {
_selectedFile = File(video.path);
_fileNameController.text = video.name;
_statusMessage = 'Video selected: ${video.name}';
});
}
} catch (e) {
_showMessage('Error picking video: $e', isError: true);
}
}
Future<void> _saveFile() async {
if (!(await _storage.hasRootAccess())) {
final status = await _storage.requestPermissions(toRootAccess: true);
if (!status) return;
}
if (_selectedFile == null) {
_showMessage('Please select a file first', isError: true);
return;
}
setState(() => _isLoading = true);
try {
final bytes = await _selectedFile!.readAsBytes();
final fileName = _fileNameController.text.isNotEmpty
? _fileNameController.text
: 'file_${DateTime.now().millisecondsSinceEpoch}${_getFileExtension(_selectedFile!.path)}';
String? path;
if (_selectedFile!.path.toLowerCase().endsWith('.mp4') ||
_selectedFile!.path.toLowerCase().endsWith('.mov')) {
path = await _storage.saveFile(
bytes: bytes,
fileName: fileName,
folderPath: _folderPath,
saveToRoot: _saveToRoot,
);
} else {
path = await _storage.saveFile(
bytes: bytes,
fileName: fileName,
folderPath: _folderPath,
saveToRoot: _saveToRoot,
);
}
if (path != null) {
_showMessage(
'File saved successfully to ${_saveToRoot ? 'root' : 'DCIM'}/$_folderPath/',
);
await _loadFileList();
} else {
_showMessage('Failed to save file', isError: true);
}
} catch (e) {
_showMessage('Error saving file: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _downloadAndSave() async {
setState(() => _isLoading = true);
try {
const imageUrl = 'https://picsum.photos/800/600';
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = 'downloaded_$timestamp.jpg';
final path = await _storage.saveFile(
bytes: response.bodyBytes,
fileName: fileName,
folderPath: _folderPath,
saveToRoot: _saveToRoot,
);
if (path != null) {
_showMessage('Image downloaded and saved!');
await _loadFileList();
} else {
_showMessage('Failed to save downloaded image', isError: true);
}
}
} catch (e) {
_showMessage('Error downloading: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _loadFileList() async {
setState(() => _isLoading = true);
try {
final files = await _storage.listFiles(
folderPath: _folderPath,
fromRoot: _saveToRoot,
);
setState(() {
_fileList = files;
_statusMessage = 'Found ${files.length} files';
});
} catch (e) {
_showMessage('Error loading files: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _retrieveFile(String fileName) async {
setState(() => _isLoading = true);
try {
final bytes = await _storage.getFile(
fileName: fileName,
folderPath: _folderPath,
fromRoot: _saveToRoot,
);
if (bytes != null) {
setState(() {
_retrievedFileBytes = bytes;
_statusMessage = 'File retrieved: $fileName (${bytes.length} bytes)';
});
_showMessage('File retrieved successfully!');
} else {
_showMessage('File not found', isError: true);
}
} catch (e) {
_showMessage('Error retrieving file: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _deleteFile(String fileName) async {
setState(() => _isLoading = true);
try {
final deleted = await _storage.deleteFile(
fileName: fileName,
folderPath: _folderPath,
fromRoot: _saveToRoot,
);
if (deleted) {
_showMessage('File deleted: $fileName');
await _loadFileList();
setState(() {
_retrievedFileBytes = null;
});
} else {
_showMessage('Failed to delete file', isError: true);
}
} catch (e) {
_showMessage('Error deleting file: $e', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
String _getFileExtension(String path) {
return path.substring(path.lastIndexOf('.'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Device Storage Manager'),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Message
if (_statusMessage.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Text(
_statusMessage,
style: TextStyle(color: Colors.blue.shade900),
),
),
// Storage Location Settings
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Storage Settings',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('Save to Root Directory'),
subtitle: Text(
_saveToRoot
? '/storage/emulated/0/$_folderPath'
: 'DCIM/$_folderPath',
),
value: _saveToRoot,
onChanged: (value) {
setState(() => _saveToRoot = value);
},
),
const SizedBox(height: 8),
TextField(
controller: _folderController,
decoration: const InputDecoration(
labelText: 'Folder Path',
border: OutlineInputBorder(),
hintText: 'e.g., PicON or MyFolder/SubFolder',
),
),
],
),
),
),
const SizedBox(height: 16),
// File Selection
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Select File',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (_selectedFile != null)
Container(
height: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child:
_selectedFile!.path.toLowerCase().endsWith(
'.mp4',
) ||
_selectedFile!.path
.toLowerCase()
.endsWith('.mov')
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.videocam, size: 60),
const SizedBox(height: 8),
Text(
_selectedFile!.path.split('/').last,
),
],
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
_selectedFile!,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 16),
TextField(
controller: _fileNameController,
decoration: const InputDecoration(
labelText: 'File Name (optional)',
border: OutlineInputBorder(),
hintText: 'Leave empty for auto-generated name',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.photo_library),
label: const Text('Gallery'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _captureImage,
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _pickVideo,
icon: const Icon(Icons.videocam),
label: const Text('Video'),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _selectedFile != null
? _saveFile
: null,
icon: const Icon(Icons.save),
label: const Text('Save to Device'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _downloadAndSave,
icon: const Icon(Icons.download),
label: const Text('Download & Save Sample'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
),
),
),
],
),
),
),
const SizedBox(height: 16),
// File List
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Saved Files',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadFileList,
),
],
),
const SizedBox(height: 12),
if (_fileList.isEmpty)
const Padding(
padding: EdgeInsets.all(20.0),
child: Text(
'No files found. Tap refresh to load.',
style: TextStyle(color: Colors.grey),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _fileList.length,
itemBuilder: (context, index) {
final fileName = _fileList[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
fileName.toLowerCase().endsWith('.mp4') ||
fileName.toLowerCase().endsWith(
'.mov',
)
? Icons.videocam
: Icons.image,
color: Colors.blue,
),
title: Text(fileName),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.visibility,
color: Colors.green,
),
onPressed: () =>
_retrieveFile(fileName),
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () =>
_deleteFile(fileName),
),
],
),
),
);
},
),
],
),
),
),
const SizedBox(height: 16),
// Retrieved File Preview
if (_retrievedFileBytes != null)
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Retrieved File Preview',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
_retrievedFileBytes!,
fit: BoxFit.contain,
),
),
),
const SizedBox(height: 8),
Text(
'Size: ${(_retrievedFileBytes!.length / 1024).toStringAsFixed(2)} KB',
style: const TextStyle(color: Colors.grey),
),
],
),
),
),
],
),
),
);
}
}