Add document creation feature with snackbar notifications and file service integration
This commit is contained in:
@@ -182,6 +182,181 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
_showRenameDialog(file);
|
_showRenameDialog(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _showCreateDocumentDialog(BuildContext context) async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
|
child: Container(
|
||||||
|
decoration: AppTheme.glassDecoration,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'New Document',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
cursorColor: AppTheme.accentColor,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Enter document name',
|
||||||
|
hintStyle: const TextStyle(color: AppTheme.secondaryText),
|
||||||
|
suffixText: '.docx',
|
||||||
|
suffixStyle: const TextStyle(
|
||||||
|
color: AppTheme.secondaryText,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: AppTheme.accentColor.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: AppTheme.secondaryText.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
borderSide: BorderSide(color: AppTheme.accentColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(color: AppTheme.primaryText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final docName = controller.text.trim();
|
||||||
|
if (docName.isNotEmpty) {
|
||||||
|
Navigator.of(context).pop(docName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Create',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: AppTheme.accentColor,
|
||||||
|
decorationThickness: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createAndOpenDocument(String currentPath) async {
|
||||||
|
final docName = await _showCreateDocumentDialog(context);
|
||||||
|
if (docName == null || docName.isEmpty || !context.mounted) return;
|
||||||
|
|
||||||
|
// Show creating snackbar
|
||||||
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? snackController;
|
||||||
|
snackController = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
backgroundColor: AppTheme.accentColor.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Creating $docName.docx...',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
duration: const Duration(days: 1),
|
||||||
|
backgroundColor: AppTheme.primaryBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final fileService = getIt<FileService>();
|
||||||
|
final fileId = await fileService.createDocument(
|
||||||
|
widget.orgId,
|
||||||
|
currentPath,
|
||||||
|
docName,
|
||||||
|
);
|
||||||
|
|
||||||
|
snackController.close();
|
||||||
|
|
||||||
|
// Refresh file list
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
LoadDirectory(orgId: widget.orgId, path: currentPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open editor with the new document
|
||||||
|
_showDocumentEditor(widget.orgId, fileId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
snackController.close();
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Failed to create document: $e',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: AppTheme.primaryBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showRenameDialog(FileItem file) async {
|
Future<void> _showRenameDialog(FileItem file) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: file.name,
|
text: file.name,
|
||||||
@@ -958,6 +1133,18 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
ModernGlassButton(
|
||||||
|
onPressed: () =>
|
||||||
|
_createAndOpenDocument(state.currentPath),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.description),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Create Document'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
ModernGlassButton(
|
ModernGlassButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final folderName =
|
final folderName =
|
||||||
@@ -1139,6 +1326,18 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
ModernGlassButton(
|
||||||
|
onPressed: () =>
|
||||||
|
_createAndOpenDocument(state.currentPath),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.description),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Create Document'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
ModernGlassButton(
|
ModernGlassButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final folderName =
|
final folderName =
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
import '../models/file_item.dart';
|
import '../models/file_item.dart';
|
||||||
import '../models/viewer_session.dart';
|
import '../models/viewer_session.dart';
|
||||||
import '../models/editor_session.dart';
|
import '../models/editor_session.dart';
|
||||||
import '../models/annotation.dart';
|
import '../models/annotation.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
|
||||||
class FileService {
|
class FileService {
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
@@ -225,4 +227,114 @@ class FileService {
|
|||||||
fromJson: (data) => null,
|
fromJson: (data) => null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an empty .docx document and returns the created file's ID
|
||||||
|
Future<String> createDocument(
|
||||||
|
String orgId,
|
||||||
|
String parentPath,
|
||||||
|
String fileName,
|
||||||
|
) async {
|
||||||
|
// Ensure filename has .docx extension
|
||||||
|
final docxName = fileName.endsWith('.docx') ? fileName : '$fileName.docx';
|
||||||
|
|
||||||
|
// Generate minimal valid DOCX file (Office Open XML format)
|
||||||
|
final bytes = _generateEmptyDocx();
|
||||||
|
|
||||||
|
// Construct proper path
|
||||||
|
String fullPath;
|
||||||
|
if (parentPath == '/') {
|
||||||
|
fullPath = '/$docxName';
|
||||||
|
} else {
|
||||||
|
final cleanParent = parentPath.endsWith('/')
|
||||||
|
? parentPath.substring(0, parentPath.length - 1)
|
||||||
|
: parentPath;
|
||||||
|
fullPath = '$cleanParent/$docxName';
|
||||||
|
}
|
||||||
|
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'path': fullPath,
|
||||||
|
'file': MultipartFile.fromBytes(bytes, filename: docxName),
|
||||||
|
});
|
||||||
|
|
||||||
|
final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files';
|
||||||
|
final response = await _apiClient.post(
|
||||||
|
endpoint,
|
||||||
|
data: formData,
|
||||||
|
fromJson: (d) => d as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the file ID from the response
|
||||||
|
return response['id'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a minimal valid DOCX file (empty document)
|
||||||
|
Uint8List _generateEmptyDocx() {
|
||||||
|
final archive = Archive();
|
||||||
|
|
||||||
|
// [Content_Types].xml - defines content types
|
||||||
|
const contentTypes =
|
||||||
|
'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="xml" ContentType="application/xml"/>
|
||||||
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||||
|
</Types>''';
|
||||||
|
archive.addFile(
|
||||||
|
ArchiveFile(
|
||||||
|
'[Content_Types].xml',
|
||||||
|
contentTypes.length,
|
||||||
|
Uint8List.fromList(contentTypes.codeUnits),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// _rels/.rels - root relationships
|
||||||
|
const rootRels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||||
|
</Relationships>''';
|
||||||
|
archive.addFile(
|
||||||
|
ArchiveFile(
|
||||||
|
'_rels/.rels',
|
||||||
|
rootRels.length,
|
||||||
|
Uint8List.fromList(rootRels.codeUnits),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// word/document.xml - the actual document content (empty)
|
||||||
|
const documentXml =
|
||||||
|
'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||||
|
<w:body>
|
||||||
|
<w:p>
|
||||||
|
<w:r>
|
||||||
|
<w:t></w:t>
|
||||||
|
</w:r>
|
||||||
|
</w:p>
|
||||||
|
</w:body>
|
||||||
|
</w:document>''';
|
||||||
|
archive.addFile(
|
||||||
|
ArchiveFile(
|
||||||
|
'word/document.xml',
|
||||||
|
documentXml.length,
|
||||||
|
Uint8List.fromList(documentXml.codeUnits),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// word/_rels/document.xml.rels - document relationships (empty but required)
|
||||||
|
const docRels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
</Relationships>''';
|
||||||
|
archive.addFile(
|
||||||
|
ArchiveFile(
|
||||||
|
'word/_rels/document.xml.rels',
|
||||||
|
docRels.length,
|
||||||
|
Uint8List.fromList(docRels.codeUnits),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encode as ZIP
|
||||||
|
final zipEncoder = ZipEncoder();
|
||||||
|
final zipData = zipEncoder.encode(archive);
|
||||||
|
return Uint8List.fromList(zipData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "6.4.1"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -896,6 +904,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.5.2"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ dependencies:
|
|||||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||||
web: ^1.1.0
|
web: ^1.1.0
|
||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
|
archive: ^4.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user