feat: better ui
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_transfer/models/file_info.dart';
|
||||
import 'package:file_transfer/routes.dart';
|
||||
import 'package:file_transfer/utils/platform_helper.dart';
|
||||
import 'package:file_transfer_sdk/file_transfer_sdk.dart';
|
||||
@@ -14,12 +15,14 @@ import 'package:toastification/toastification.dart';
|
||||
|
||||
class HomePageController extends GetxController {
|
||||
final _sharedFile = Rxn<SharedFile>();
|
||||
final _fileInfo = Rxn<FileInfo>();
|
||||
final _isUploading = false.obs;
|
||||
final _error = RxnString();
|
||||
final _isDragging = false.obs;
|
||||
final dropzoneController = Rxn<DropzoneViewController>();
|
||||
|
||||
SharedFile? get sharedFile => _sharedFile.value;
|
||||
FileInfo? get fileInfo => _fileInfo.value;
|
||||
bool get isUploading => _isUploading.value;
|
||||
String? get error => _error.value;
|
||||
bool get isDragging => _isDragging.value;
|
||||
@@ -38,11 +41,44 @@ class HomePageController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleDroppedFile(dynamic file) async {
|
||||
_isDragging.value = false;
|
||||
Future<void> _uploadFile(Uint8List bytes, String filename) async {
|
||||
_isUploading.value = true;
|
||||
_error.value = null;
|
||||
|
||||
try {
|
||||
final mimeType = lookupMimeType(
|
||||
filename,
|
||||
headerBytes: bytes.take(8).toList(),
|
||||
);
|
||||
|
||||
final sharedFile = await shareFile(
|
||||
ndk: Get.find(),
|
||||
bytes: bytes,
|
||||
contentType: mimeType,
|
||||
filename: filename,
|
||||
);
|
||||
|
||||
_sharedFile.value = sharedFile;
|
||||
_fileInfo.value = null;
|
||||
} catch (e) {
|
||||
_error.value = e.toString();
|
||||
} finally {
|
||||
_isUploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _setFileInfo(Uint8List bytes, String filename, String? mimeType) {
|
||||
_fileInfo.value = FileInfo(
|
||||
name: filename,
|
||||
size: bytes.length,
|
||||
mimeType: mimeType,
|
||||
bytes: bytes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleDroppedFile(dynamic file) async {
|
||||
_isDragging.value = false;
|
||||
|
||||
try {
|
||||
String filename;
|
||||
Uint8List bytes;
|
||||
@@ -66,15 +102,7 @@ class HomePageController extends GetxController {
|
||||
headerBytes: bytes.take(8).toList(),
|
||||
);
|
||||
|
||||
final sharedFile = await shareFile(
|
||||
ndk: Get.find(),
|
||||
bytes: bytes,
|
||||
contentType: mimeType,
|
||||
filename: filename,
|
||||
);
|
||||
|
||||
_sharedFile.value = sharedFile;
|
||||
_isUploading.value = false;
|
||||
_setFileInfo(bytes, filename, mimeType);
|
||||
} catch (e) {
|
||||
_error.value = e.toString();
|
||||
_isUploading.value = false;
|
||||
@@ -90,45 +118,37 @@ class HomePageController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> pickAndShareFile() async {
|
||||
_isUploading.value = true;
|
||||
_error.value = null;
|
||||
final pickResult = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
withData: true,
|
||||
);
|
||||
|
||||
try {
|
||||
final pickResult = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (pickResult == null || pickResult.files.isEmpty) {
|
||||
_isUploading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final file = pickResult.files.first;
|
||||
if (file.bytes == null) {
|
||||
_error.value = 'Unable to read file data';
|
||||
_isUploading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final mimeType = lookupMimeType(
|
||||
file.name,
|
||||
headerBytes: file.bytes?.take(8).toList(),
|
||||
);
|
||||
|
||||
final sharedFile = await shareFile(
|
||||
ndk: Get.find(),
|
||||
bytes: file.bytes!,
|
||||
contentType: mimeType,
|
||||
filename: file.name,
|
||||
);
|
||||
|
||||
_sharedFile.value = sharedFile;
|
||||
_isUploading.value = false;
|
||||
} catch (e) {
|
||||
_error.value = e.toString();
|
||||
_isUploading.value = false;
|
||||
if (pickResult == null || pickResult.files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final file = pickResult.files.first;
|
||||
if (file.bytes == null) {
|
||||
_error.value = 'Unable to read file data';
|
||||
return;
|
||||
}
|
||||
|
||||
final mimeType = lookupMimeType(
|
||||
file.name,
|
||||
headerBytes: file.bytes?.take(8).toList(),
|
||||
);
|
||||
|
||||
_setFileInfo(file.bytes!, file.name, mimeType);
|
||||
}
|
||||
|
||||
Future<void> startUpload() async {
|
||||
final fileInfo = _fileInfo.value;
|
||||
if (fileInfo == null) {
|
||||
_error.value = 'No file selected';
|
||||
return;
|
||||
}
|
||||
|
||||
await _uploadFile(fileInfo.bytes, fileInfo.name);
|
||||
}
|
||||
|
||||
void copyToClipboard(String text, String label) {
|
||||
@@ -147,6 +167,7 @@ class HomePageController extends GetxController {
|
||||
|
||||
void reset() {
|
||||
_sharedFile.value = null;
|
||||
_fileInfo.value = null;
|
||||
_error.value = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FileInfo {
|
||||
final String name;
|
||||
final int size;
|
||||
final String? mimeType;
|
||||
final DateTime? lastModified;
|
||||
final Uint8List bytes;
|
||||
|
||||
FileInfo({
|
||||
required this.name,
|
||||
required this.size,
|
||||
this.mimeType,
|
||||
this.lastModified,
|
||||
required this.bytes,
|
||||
});
|
||||
}
|
||||
@@ -83,7 +83,35 @@ class ReadyView extends GetView<FileShareController> {
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (metadata.filename != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Name:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: isSmallScreen ? 13 : 14,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
metadata.filename!,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 13 : 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
if (metadata.fileType != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:file_transfer/controllers/home_controller.dart';
|
||||
import 'package:file_transfer/models/file_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class FileInfoView extends GetView<HomePageController> {
|
||||
const FileInfoView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fileInfo = controller.fileInfo;
|
||||
final isSmallScreen = MediaQuery.of(context).size.width < 600;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('File Ready to Upload'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.reset,
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: isSmallScreen ? 16 : 24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).padding.top),
|
||||
Icon(
|
||||
Icons.insert_drive_file_rounded,
|
||||
size: isSmallScreen ? 64 : 80,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 20 : 24),
|
||||
Text(
|
||||
'File selected',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 16 : 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 16 : 24),
|
||||
_buildFileInfoCard(
|
||||
context,
|
||||
isSmallScreen,
|
||||
fileInfo!,
|
||||
colorScheme,
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 24 : 32),
|
||||
Obx(
|
||||
() => FilledButton.icon(
|
||||
onPressed: controller.isUploading
|
||||
? null
|
||||
: () => controller.startUpload(),
|
||||
icon: controller.isUploading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.cloud_upload),
|
||||
label: Text(
|
||||
controller.isUploading ? 'Uploading...' : 'Upload File',
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 6 : 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: controller.reset,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Select Different File'),
|
||||
),
|
||||
Obx(() {
|
||||
if (controller.error != null) {
|
||||
return _buildErrorBox(context, isSmallScreen, colorScheme);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorBox(
|
||||
BuildContext context,
|
||||
bool isSmallScreen,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isSmallScreen ? 10 : 12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: colorScheme.onErrorContainer),
|
||||
SizedBox(width: isSmallScreen ? 8 : 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.error!,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontSize: isSmallScreen ? 12 : 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileInfoCard(
|
||||
BuildContext context,
|
||||
bool isSmallScreen,
|
||||
FileInfo fileInfo,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Name:', fileInfo.name, isSmallScreen, colorScheme),
|
||||
SizedBox(height: isSmallScreen ? 12 : 16),
|
||||
_buildInfoRow(
|
||||
'Size:',
|
||||
_formatFileSize(fileInfo.size),
|
||||
isSmallScreen,
|
||||
colorScheme,
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 12 : 16),
|
||||
_buildInfoRow(
|
||||
'Type:',
|
||||
fileInfo.mimeType ?? 'Unknown',
|
||||
isSmallScreen,
|
||||
colorScheme,
|
||||
),
|
||||
if (fileInfo.lastModified != null) ...[
|
||||
SizedBox(height: isSmallScreen ? 12 : 16),
|
||||
_buildInfoRow(
|
||||
'Modified:',
|
||||
_formatDate(fileInfo.lastModified!),
|
||||
isSmallScreen,
|
||||
colorScheme,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String label,
|
||||
String value,
|
||||
bool isSmallScreen,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: isSmallScreen ? 13 : 14,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 6 : 8),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 11 : 12,
|
||||
fontFamily: 'monospace',
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} '
|
||||
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export 'upload_view.dart';
|
||||
export 'share_link_view.dart';
|
||||
export 'file_info_view.dart';
|
||||
|
||||
@@ -12,6 +12,9 @@ class HomePage extends GetView<HomePageController> {
|
||||
if (controller.sharedFile != null) {
|
||||
return const ShareLinkView();
|
||||
}
|
||||
if (controller.fileInfo != null) {
|
||||
return const FileInfoView();
|
||||
}
|
||||
return const UploadView();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user