pion_resumable_upload 0.1.0
pion_resumable_upload: ^0.1.0 copied to clipboard
Resumable, fault-tolerant chunked file uploads for Dart, speaking the resumable.js protocol against a pion/laravel-chunk-upload backend.
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:pion_resumable_upload/pion_resumable_upload.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Resumable Upload Example',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const UploadPage(),
);
}
}
class UploadPage extends StatefulWidget {
const UploadPage({super.key});
@override
State<UploadPage> createState() => _UploadPageState();
}
class _UploadPageState extends State<UploadPage> {
final TextEditingController _urlController = TextEditingController(
text: 'https://api.example.com/upload',
);
UploadClient? _client;
UploadTask? _task;
UploadProgress? _progress;
String? _fileName;
String? _error;
UploadTaskState get _state => _progress?.state ?? UploadTaskState.idle;
Future<void> _pickAndUpload() async {
final picked = await FilePicker.platform.pickFiles();
final path = picked?.files.single.path;
if (path == null) {
return;
}
await _client?.close();
final client = UploadClient(
config: UploadConfig(uploadUrl: Uri.parse(_urlController.text.trim())),
);
final task = client.createTask(File(path));
setState(() {
_client = client;
_task = task;
_fileName = picked!.files.single.name;
_error = null;
_progress = null;
});
task.progress.listen((p) {
if (mounted) {
setState(() => _progress = p);
}
});
try {
await task.start();
} on ResumableUploadException catch (e) {
if (mounted) {
setState(() => _error = e.message);
}
}
}
@override
void dispose() {
_urlController.dispose();
_client?.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final progress = _progress;
final task = _task;
final isUploading = _state == UploadTaskState.uploading;
final isPaused = _state == UploadTaskState.paused;
return Scaffold(
appBar: AppBar(title: const Text('Resumable Upload')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _urlController,
decoration: const InputDecoration(
labelText: 'Upload URL',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: isUploading ? null : _pickAndUpload,
icon: const Icon(Icons.upload_file),
label: const Text('Pick a file and upload'),
),
const SizedBox(height: 24),
if (_fileName != null) Text('File: $_fileName'),
const SizedBox(height: 8),
if (progress != null) ...[
LinearProgressIndicator(value: progress.fraction),
const SizedBox(height: 8),
Text(
'${progress.percentage.toStringAsFixed(1)}% • '
'${progress.completedChunks}/${progress.totalChunks} chunks • '
'${progress.state.name}',
),
],
if (_error != null) ...[
const SizedBox(height: 8),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
onPressed: isUploading ? task!.pause : null,
child: const Text('Pause'),
),
OutlinedButton(
onPressed: isPaused ? () => task!.resume() : null,
child: const Text('Resume'),
),
OutlinedButton(
onPressed: (isUploading || isPaused) ? task!.cancel : null,
child: const Text('Cancel'),
),
],
),
],
),
),
);
}
}