diff --git a/b0esche_cloud/lib/pages/file_explorer.dart b/b0esche_cloud/lib/pages/file_explorer.dart index b0b23d1..b389fd9 100644 --- a/b0esche_cloud/lib/pages/file_explorer.dart +++ b/b0esche_cloud/lib/pages/file_explorer.dart @@ -182,6 +182,181 @@ class _FileExplorerState extends State { _showRenameDialog(file); } + Future _showCreateDocumentDialog(BuildContext context) async { + final TextEditingController controller = TextEditingController(); + return showDialog( + 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? 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(); + final fileId = await fileService.createDocument( + widget.orgId, + currentPath, + docName, + ); + + snackController.close(); + + // Refresh file list + if (context.mounted) { + context.read().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 _showRenameDialog(FileItem file) async { final TextEditingController controller = TextEditingController( text: file.name, @@ -958,6 +1133,18 @@ class _FileExplorerState extends State { ), ), 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( onPressed: () async { final folderName = @@ -1139,6 +1326,18 @@ class _FileExplorerState extends State { ), ), 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( onPressed: () async { final folderName = diff --git a/b0esche_cloud/lib/services/file_service.dart b/b0esche_cloud/lib/services/file_service.dart index 5a570ec..b81aae8 100644 --- a/b0esche_cloud/lib/services/file_service.dart +++ b/b0esche_cloud/lib/services/file_service.dart @@ -1,9 +1,11 @@ +import 'dart:typed_data'; import '../models/file_item.dart'; import '../models/viewer_session.dart'; import '../models/editor_session.dart'; import '../models/annotation.dart'; import 'api_client.dart'; import 'package:dio/dio.dart'; +import 'package:archive/archive.dart'; class FileService { final ApiClient _apiClient; @@ -225,4 +227,114 @@ class FileService { fromJson: (data) => null, ); } + + /// Creates an empty .docx document and returns the created file's ID + Future 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, + ); + + // 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 = + ''' + + + + +'''; + archive.addFile( + ArchiveFile( + '[Content_Types].xml', + contentTypes.length, + Uint8List.fromList(contentTypes.codeUnits), + ), + ); + + // _rels/.rels - root relationships + const rootRels = ''' + + +'''; + archive.addFile( + ArchiveFile( + '_rels/.rels', + rootRels.length, + Uint8List.fromList(rootRels.codeUnits), + ), + ); + + // word/document.xml - the actual document content (empty) + const documentXml = + ''' + + + + + + + + +'''; + archive.addFile( + ArchiveFile( + 'word/document.xml', + documentXml.length, + Uint8List.fromList(documentXml.codeUnits), + ), + ); + + // word/_rels/document.xml.rels - document relationships (empty but required) + const docRels = ''' + +'''; + 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); + } } diff --git a/b0esche_cloud/pubspec.lock b/b0esche_cloud/pubspec.lock index ba17cd5..9692117 100644 --- a/b0esche_cloud/pubspec.lock +++ b/b0esche_cloud/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + archive: + dependency: "direct main" + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -896,6 +904,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" provider: dependency: "direct main" description: diff --git a/b0esche_cloud/pubspec.yaml b/b0esche_cloud/pubspec.yaml index 113342d..4239cf9 100644 --- a/b0esche_cloud/pubspec.yaml +++ b/b0esche_cloud/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: syncfusion_flutter_pdfviewer: ^31.1.21 web: ^1.1.0 http: ^1.2.0 + archive: ^4.0.4 dev_dependencies: flutter_test: