filesaverplus 0.0.5
filesaverplus: ^0.0.5 copied to clipboard
FileSaverPlus is a Flutter plugin for saving files to device storage with cross-platform support for Android, iOS, Web, Windows, Linux, and macOS.
import 'dart:convert';
import 'package:filesaverplus/filesaverplus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FileSaverPlus Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const DemoPage(),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final _fileSaver = FileSaverPlus();
final List<_LogEntry> _log = [];
bool _busy = false;
// ── helpers ────────────────────────────────────────────────────────────────
Uint8List _utf8(String s) => Uint8List.fromList(utf8.encode(s));
void _addLog(String message, {bool isError = false}) {
setState(() => _log.insert(0, _LogEntry(message, isError: isError)));
}
Future<void> _run(Future<void> Function() action) async {
if (_busy) return;
setState(() => _busy = true);
try {
await action();
} finally {
setState(() => _busy = false);
}
}
// ── example actions ────────────────────────────────────────────────────────
/// 1. Save a single plain-text file.
Future<void> _saveSingleText() => _run(() async {
const content = 'Hello from FileSaverPlus!\n\nThis is a plain-text file.';
final path = await _fileSaver.saveFile(
_utf8(content),
'hello.txt',
'text/plain',
);
_addLog('✓ Saved text file\n → $path');
});
/// 2. Save a single HTML file.
Future<void> _saveSingleHtml() => _run(() async {
const content = '''<!DOCTYPE html>
<html>
<head><title>FileSaverPlus</title></head>
<body>
<h1>Hello, FileSaverPlus!</h1>
<p>This HTML file was saved by the FileSaverPlus plugin.</p>
</body>
</html>''';
final path = await _fileSaver.saveFile(
_utf8(content),
'demo.html',
'text/html',
);
_addLog('✓ Saved HTML file\n → $path');
});
/// 3. Save a JSON file.
Future<void> _saveSingleJson() => _run(() async {
final data = jsonEncode({
'plugin': 'filesaverplus',
'version': '0.0.5',
'platforms': ['android', 'ios', 'web', 'windows', 'linux', 'macos'],
'savedAt': DateTime.now().toIso8601String(),
});
final path = await _fileSaver.saveFile(
_utf8(data),
'plugin_info.json',
'application/json',
);
_addLog('✓ Saved JSON file\n → $path');
});
/// 4. Save multiple files in one call (HTML + TXT + JSON).
Future<void> _saveMultiple() => _run(() async {
final paths = await _fileSaver.saveMultipleFiles(
dataList: [
_utf8('<h1>Report</h1><p>Generated by FileSaverPlus.</p>'),
_utf8('Summary\n-------\nMultiple files saved in one call.'),
_utf8(jsonEncode({'status': 'ok', 'count': 3})),
],
fileNameList: ['report.html', 'summary.txt', 'result.json'],
mimeTypeList: ['text/html', 'text/plain', 'application/json'],
);
_addLog('✓ Saved ${paths.length} files');
for (final p in paths) {
_addLog(' → $p');
}
});
/// 5. Duplicate-name handling — plugin auto-appends _2, _3, …
Future<void> _saveDuplicates() => _run(() async {
final paths = await _fileSaver.saveMultipleFiles(
dataList: [
_utf8('File A content'),
_utf8('File B content'),
_utf8('File C content'),
],
fileNameList: ['data.txt', 'data.txt', 'data.txt'],
mimeTypeList: ['text/plain', 'text/plain', 'text/plain'],
);
_addLog('✓ Duplicate names auto-renamed:');
for (final p in paths) {
_addLog(' → $p');
}
});
/// 6. Show platform info and battery level.
Future<void> _showDeviceInfo() => _run(() async {
final version = await _fileSaver.platformVersion;
final battery = await _fileSaver.batteryPercentage;
_addLog(
'✓ Platform: $version\n'
' Battery: ${battery != null ? '$battery%' : 'not available'}',
);
});
/// 7. Error demo — passes empty data to trigger an ArgumentError.
Future<void> _triggerError() => _run(() async {
try {
await _fileSaver.saveFile(
Uint8List(0), // intentionally empty — triggers ArgumentError
'empty.txt',
'text/plain',
);
} on ArgumentError catch (e) {
_addLog('✗ ArgumentError (expected): $e', isError: true);
} on PlatformException catch (e) {
_addLog('✗ PlatformException [${e.code}]: ${e.message}', isError: true);
}
});
// ── UI ─────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FileSaverPlus Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
if (_log.isNotEmpty)
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Clear log',
onPressed: () => setState(() => _log.clear()),
),
],
),
body: Column(
children: [
// ── action list ─────────────────────────────────────────────────
Expanded(
flex: 2,
child: ListView(
padding: const EdgeInsets.all(12),
children: [
_SectionHeader('Single file'),
_ActionTile(
icon: Icons.text_snippet_outlined,
title: 'Save text file',
subtitle: 'hello.txt — text/plain',
onTap: _busy ? null : _saveSingleText,
),
_ActionTile(
icon: Icons.html_outlined,
title: 'Save HTML file',
subtitle: 'demo.html — text/html',
onTap: _busy ? null : _saveSingleHtml,
),
_ActionTile(
icon: Icons.data_object_outlined,
title: 'Save JSON file',
subtitle: 'plugin_info.json — application/json',
onTap: _busy ? null : _saveSingleJson,
),
const SizedBox(height: 8),
_SectionHeader('Multiple files'),
_ActionTile(
icon: Icons.file_copy_outlined,
title: 'Save 3 files at once',
subtitle: 'report.html + summary.txt + result.json',
onTap: _busy ? null : _saveMultiple,
),
_ActionTile(
icon: Icons.drive_file_rename_outline,
title: 'Duplicate name handling',
subtitle: 'Three "data.txt" → auto-renamed to _2, _3',
onTap: _busy ? null : _saveDuplicates,
),
const SizedBox(height: 8),
_SectionHeader('Device info'),
_ActionTile(
icon: Icons.info_outline,
title: 'Platform version & battery',
subtitle: 'getPlatformVersion · getBatteryPercentage',
onTap: _busy ? null : _showDeviceInfo,
),
const SizedBox(height: 8),
_SectionHeader('Error handling'),
_ActionTile(
icon: Icons.error_outline,
title: 'Trigger ArgumentError',
subtitle: 'Passes empty data — shows caught error below',
onTap: _busy ? null : _triggerError,
isDestructive: true,
),
],
),
),
const Divider(height: 1),
// ── log panel ──────────────────────────────────────────────────
Expanded(
child:
_log.isEmpty
? const Center(
child: Text(
'Tap an action above — results appear here.',
style: TextStyle(color: Colors.grey),
),
)
: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: _log.length,
separatorBuilder: (_, __) => const SizedBox(height: 4),
itemBuilder: (context, i) {
final entry = _log[i];
return Text(
entry.message,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color:
entry.isError ? Colors.red : Colors.green[800],
),
);
},
),
),
if (_busy)
const LinearProgressIndicator()
else
const SizedBox(height: 4),
],
),
);
}
}
// ── small widgets ─────────────────────────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
const _SectionHeader(this.label);
final String label;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
label.toUpperCase(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
class _ActionTile extends StatelessWidget {
const _ActionTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.isDestructive = false,
});
final IconData icon;
final String title;
final String subtitle;
final VoidCallback? onTap;
final bool isDestructive;
@override
Widget build(BuildContext context) {
final color =
isDestructive ? Colors.red : Theme.of(context).colorScheme.primary;
return Card(
margin: const EdgeInsets.symmetric(vertical: 3),
child: ListTile(
leading: Icon(icon, color: onTap == null ? Colors.grey : color),
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
color: onTap == null ? Colors.grey : null,
),
),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
trailing: Icon(
Icons.chevron_right,
color: onTap == null ? Colors.grey : null,
),
onTap: onTap,
),
);
}
}
class _LogEntry {
const _LogEntry(this.message, {this.isError = false});
final String message;
final bool isError;
}