feat: better ui
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
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/routes.dart';
|
||||||
import 'package:file_transfer/utils/platform_helper.dart';
|
import 'package:file_transfer/utils/platform_helper.dart';
|
||||||
import 'package:file_transfer_sdk/file_transfer_sdk.dart';
|
import 'package:file_transfer_sdk/file_transfer_sdk.dart';
|
||||||
@@ -14,12 +15,14 @@ import 'package:toastification/toastification.dart';
|
|||||||
|
|
||||||
class HomePageController extends GetxController {
|
class HomePageController extends GetxController {
|
||||||
final _sharedFile = Rxn<SharedFile>();
|
final _sharedFile = Rxn<SharedFile>();
|
||||||
|
final _fileInfo = Rxn<FileInfo>();
|
||||||
final _isUploading = false.obs;
|
final _isUploading = false.obs;
|
||||||
final _error = RxnString();
|
final _error = RxnString();
|
||||||
final _isDragging = false.obs;
|
final _isDragging = false.obs;
|
||||||
final dropzoneController = Rxn<DropzoneViewController>();
|
final dropzoneController = Rxn<DropzoneViewController>();
|
||||||
|
|
||||||
SharedFile? get sharedFile => _sharedFile.value;
|
SharedFile? get sharedFile => _sharedFile.value;
|
||||||
|
FileInfo? get fileInfo => _fileInfo.value;
|
||||||
bool get isUploading => _isUploading.value;
|
bool get isUploading => _isUploading.value;
|
||||||
String? get error => _error.value;
|
String? get error => _error.value;
|
||||||
bool get isDragging => _isDragging.value;
|
bool get isDragging => _isDragging.value;
|
||||||
@@ -38,11 +41,44 @@ class HomePageController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleDroppedFile(dynamic file) async {
|
Future<void> _uploadFile(Uint8List bytes, String filename) async {
|
||||||
_isDragging.value = false;
|
|
||||||
_isUploading.value = true;
|
_isUploading.value = true;
|
||||||
_error.value = null;
|
_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 {
|
try {
|
||||||
String filename;
|
String filename;
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
@@ -66,15 +102,7 @@ class HomePageController extends GetxController {
|
|||||||
headerBytes: bytes.take(8).toList(),
|
headerBytes: bytes.take(8).toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final sharedFile = await shareFile(
|
_setFileInfo(bytes, filename, mimeType);
|
||||||
ndk: Get.find(),
|
|
||||||
bytes: bytes,
|
|
||||||
contentType: mimeType,
|
|
||||||
filename: filename,
|
|
||||||
);
|
|
||||||
|
|
||||||
_sharedFile.value = sharedFile;
|
|
||||||
_isUploading.value = false;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error.value = e.toString();
|
_error.value = e.toString();
|
||||||
_isUploading.value = false;
|
_isUploading.value = false;
|
||||||
@@ -90,45 +118,37 @@ class HomePageController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pickAndShareFile() async {
|
Future<void> pickAndShareFile() async {
|
||||||
_isUploading.value = true;
|
final pickResult = await FilePicker.platform.pickFiles(
|
||||||
_error.value = null;
|
type: FileType.any,
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
if (pickResult == null || pickResult.files.isEmpty) {
|
||||||
final pickResult = await FilePicker.platform.pickFiles(
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
void copyToClipboard(String text, String label) {
|
||||||
@@ -147,6 +167,7 @@ class HomePageController extends GetxController {
|
|||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
_sharedFile.value = null;
|
_sharedFile.value = null;
|
||||||
|
_fileInfo.value = null;
|
||||||
_error.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(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
|
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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) ...[
|
if (metadata.fileType != null) ...[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
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 'upload_view.dart';
|
||||||
export 'share_link_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) {
|
if (controller.sharedFile != null) {
|
||||||
return const ShareLinkView();
|
return const ShareLinkView();
|
||||||
}
|
}
|
||||||
|
if (controller.fileInfo != null) {
|
||||||
|
return const FileInfoView();
|
||||||
|
}
|
||||||
return const UploadView();
|
return const UploadView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user