diff --git a/b0esche_cloud/lib/pages/document_viewer.dart b/b0esche_cloud/lib/pages/document_viewer.dart index 7249425..2fb3496 100644 --- a/b0esche_cloud/lib/pages/document_viewer.dart +++ b/b0esche_cloud/lib/pages/document_viewer.dart @@ -291,9 +291,8 @@ class _DocumentViewerModalState extends State { label: const Text('Download File'), onPressed: () { // Open file download - final Uri url = Uri.parse(state.viewUrl); // In a real implementation, you'd use url_launcher - // launchUrl(url); + // launchUrl(state.viewUrl); }, ), ], @@ -498,34 +497,174 @@ class _DocumentViewerState extends State { ); } if (state is DocumentViewerReady) { - if (state.caps.isPdf) { - return BlocBuilder( - builder: (context, sessionState) { - String? token; - if (sessionState is SessionActive) { - token = sessionState.token; - } + return BlocBuilder( + builder: (context, sessionState) { + String? token; + if (sessionState is SessionActive) { + token = sessionState.token; + } + + if (state.caps.isPdf) { + // PDF viewer using SfPdfViewer return SfPdfViewer.network( state.viewUrl.toString(), headers: token != null ? {'Authorization': 'Bearer $token'} : {}, + onDocumentLoadFailed: (details) {}, + onDocumentLoaded: (PdfDocumentLoadedDetails details) {}, ); - }, - ); - } else { - // Placeholder for office docs iframe - return Container( - color: AppTheme.secondaryText, - child: Center( - child: Text( - 'Office Document Viewer\n(URL: ${state.viewUrl})', - textAlign: TextAlign.center, - style: const TextStyle(color: AppTheme.primaryText), - ), - ), - ); - } + } else if (state.caps.isImage) { + // Image viewer + return Container( + color: AppTheme.primaryBackground, + child: InteractiveViewer( + minScale: 0.5, + maxScale: 4.0, + child: Image.network( + state.viewUrl.toString(), + headers: token != null + ? {'Authorization': 'Bearer $token'} + : {}, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text( + 'Failed to load image', + style: TextStyle(color: Colors.red[400]), + ), + ); + }, + ), + ), + ); + } else if (state.caps.isText) { + // Text file viewer + return FutureBuilder( + future: _fetchTextContent( + state.viewUrl.toString(), + token ?? '', + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + AppTheme.accentColor, + ), + ), + ); + } + if (snapshot.hasError) { + return Center( + child: Text( + 'Error loading text: ${snapshot.error}', + style: TextStyle(color: Colors.red[400]), + ), + ); + } + return SingleChildScrollView( + child: Container( + color: AppTheme.primaryBackground, + padding: const EdgeInsets.all(16), + child: SelectableText( + snapshot.data ?? '', + style: TextStyle( + color: AppTheme.primaryText, + fontFamily: 'Courier New', + fontSize: 13, + height: 1.5, + ), + ), + ), + ); + }, + ); + } else if (state.caps.isOffice) { + // Office document viewer - show placeholder with download option + return Container( + color: AppTheme.primaryBackground, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + state.caps.mimeType.contains('word') + ? Icons.description + : state.caps.mimeType.contains('sheet') + ? Icons.table_chart + : Icons.folder_open, + size: 64, + color: AppTheme.accentColor, + ), + const SizedBox(height: 16), + Text( + 'Office Document', + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Download to view in your office application', + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 14, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + icon: const Icon(Icons.download), + label: const Text('Download File'), + onPressed: () { + // Open file download + // In a real implementation, you'd use url_launcher + // launchUrl(state.viewUrl); + }, + ), + ], + ), + ), + ); + } else { + // Unknown file type + return Container( + color: AppTheme.primaryBackground, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.description, + size: 64, + color: AppTheme.accentColor, + ), + const SizedBox(height: 16), + Text( + 'File Type Not Supported', + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'MIME Type: ${state.caps.mimeType}', + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } + }, + ); } return const Center(child: Text('No document loaded')); }, @@ -534,6 +673,23 @@ class _DocumentViewerState extends State { ); } + Future _fetchTextContent(String url, String token) async { + try { + final response = await http.get( + Uri.parse(url), + headers: {'Authorization': 'Bearer $token'}, + ).timeout(const Duration(seconds: 30)); + + if (response.statusCode == 200) { + return response.body; + } else { + throw Exception('Failed to load text: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Error fetching text: $e'); + } + } + @override void dispose() { _viewerBloc.close(); diff --git a/b0esche_cloud/lib/repositories/mock_file_repository.dart b/b0esche_cloud/lib/repositories/mock_file_repository.dart deleted file mode 100644 index 6adde7e..0000000 --- a/b0esche_cloud/lib/repositories/mock_file_repository.dart +++ /dev/null @@ -1,292 +0,0 @@ -import '../models/file_item.dart'; -import '../models/viewer_session.dart'; -import '../models/editor_session.dart'; -import '../models/annotation.dart'; -import '../models/document_capabilities.dart'; -import '../models/api_error.dart'; -import '../repositories/file_repository.dart'; -import 'package:path/path.dart' as p; -import 'package:uuid/uuid.dart'; - -class MockFileRepository implements FileRepository { - final Map> _orgFiles = {}; - final _uuid = const Uuid(); - - List _getFilesForOrg(String orgId) { - if (!_orgFiles.containsKey(orgId)) { - // Initialize with different files per org - if (orgId == 'org1') { - _orgFiles[orgId] = [ - FileItem( - id: _uuid.v4(), - name: 'Personal Documents', - path: '/Personal Documents', - type: FileType.folder, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'Photos', - path: '/Photos', - type: FileType.folder, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'resume.pdf', - path: '/resume.pdf', - type: FileType.file, - size: 1024, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'notes.txt', - path: '/notes.txt', - type: FileType.file, - size: 256, - lastModified: DateTime.now(), - ), - ]; - } else if (orgId == 'org2') { - _orgFiles[orgId] = [ - FileItem( - id: _uuid.v4(), - name: 'Company Reports', - path: '/Company Reports', - type: FileType.folder, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'annual_report.pdf', - path: '/annual_report.pdf', - type: FileType.file, - size: 2048, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'presentation.pptx', - path: '/presentation.pptx', - type: FileType.file, - size: 4096, - lastModified: DateTime.now(), - ), - ]; - } else if (orgId == 'org3') { - _orgFiles[orgId] = [ - FileItem( - id: _uuid.v4(), - name: 'Project Code', - path: '/Project Code', - type: FileType.folder, - lastModified: DateTime.now(), - ), - FileItem( - id: _uuid.v4(), - name: 'side_project.dart', - path: '/side_project.dart', - type: FileType.file, - size: 512, - lastModified: DateTime.now(), - ), - ]; - } else { - // Default for new orgs - _orgFiles[orgId] = []; - } - } - return _orgFiles[orgId]!; - } - - @override - Future> getFiles(String orgId, String path) async { - await Future.delayed(const Duration(seconds: 1)); - final files = _getFilesForOrg(orgId); - if (path == '/') { - return files.where((f) => !f.path.substring(1).contains('/')).toList(); - } else { - return files - .where((f) => f.path.startsWith('$path/') && f.path != path) - .toList(); - } - } - - @override - Future getFile(String orgId, String path) async { - final files = _getFilesForOrg(orgId); - final index = files.indexWhere((f) => f.path == path); - return index != -1 ? files[index] : null; - } - - @override - Future requestEditorSession( - String orgId, - String fileId, - ) async { - await Future.delayed(const Duration(seconds: 1)); - // Mock: determine editability - final isEditable = - fileId.endsWith('.docx') || - fileId.endsWith('.xlsx') || - fileId.endsWith('.pptx'); - final editUrl = Uri.parse( - 'https://office.b0esche.cloud/editor/$orgId/$fileId?editable=$isEditable', - ); - final expiresAt = DateTime.now().add(const Duration(minutes: 30)); - return EditorSession( - editUrl: editUrl, - token: 'mock-editor-token', - readOnly: !isEditable, - expiresAt: expiresAt, - ); - } - - @override - Future deleteFile(String orgId, String path) async { - final files = _getFilesForOrg(orgId); - files.removeWhere((f) => f.path == path); - } - - @override - Future createFolder( - String orgId, - String parentPath, - String folderName, - ) async { - await Future.delayed(const Duration(seconds: 1)); - final normalizedName = folderName.startsWith('/') - ? folderName.substring(1) - : folderName; - final newPath = parentPath == '/' - ? '/$normalizedName' - : '$parentPath/$normalizedName'; - final files = _getFilesForOrg(orgId); - files.add( - FileItem( - id: _uuid.v4(), - name: normalizedName, - path: newPath, - type: FileType.folder, - lastModified: DateTime.now(), - ), - ); - } - - @override - Future moveFile( - String orgId, - String sourcePath, - String targetPath, - ) async { - await Future.delayed(const Duration(seconds: 1)); - final files = _getFilesForOrg(orgId); - final fileIndex = files.indexWhere((f) => f.path == sourcePath); - if (fileIndex != -1) { - final file = files[fileIndex]; - final newName = file.path.split('/').last; - final newPath = targetPath == '/' ? '/$newName' : '$targetPath/$newName'; - files[fileIndex] = FileItem( - id: file.id, - name: file.name, - path: newPath, - type: file.type, - size: file.size, - lastModified: DateTime.now(), - ); - } - } - - @override - Future renameFile(String orgId, String path, String newName) async { - await Future.delayed(const Duration(seconds: 1)); - final files = _getFilesForOrg(orgId); - final fileIndex = files.indexWhere((f) => f.path == path); - if (fileIndex != -1) { - final file = files[fileIndex]; - final parentPath = p.dirname(path); - final newPath = parentPath == '.' ? '/$newName' : '$parentPath/$newName'; - files[fileIndex] = FileItem( - id: file.id, - name: newName, - path: newPath, - type: file.type, - size: file.size, - lastModified: DateTime.now(), - ); - } - } - - @override - Future> searchFiles(String orgId, String query) async { - await Future.delayed(const Duration(seconds: 1)); - final files = _getFilesForOrg(orgId); - return files - .where((f) => f.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - } - - @override - Future uploadFile(String orgId, FileItem file) async { - await Future.delayed(const Duration(seconds: 1)); - final files = _getFilesForOrg(orgId); - files.add( - FileItem( - id: _uuid.v4(), - name: file.name, - path: file.path, - type: file.type, - size: file.size, - lastModified: file.lastModified, - ), - ); - } - - @override - Future requestViewerSession( - String orgId, - String fileId, - ) async { - await Future.delayed(const Duration(seconds: 1)); - if (fileId.contains('forbidden')) { - throw ApiError( - code: 'permission_denied', - message: 'Access denied', - status: 403, - ); - } - if (fileId.contains('notfound')) { - throw ApiError(code: 'not_found', message: 'File not found', status: 404); - } - // Mock: assume fileId is path, determine if PDF - final isPdf = fileId.endsWith('.pdf'); - final caps = DocumentCapabilities( - canEdit: !isPdf && (fileId.endsWith('.docx') || fileId.endsWith('.xlsx')), - canAnnotate: isPdf, - isPdf: isPdf, - ); - // Mock URL - final viewUrl = Uri.parse( - 'https://office.b0esche.cloud/viewer/$orgId/$fileId', - ); - final token = 'mock-viewer-token'; - final expiresAt = DateTime.now().add(const Duration(minutes: 30)); - return ViewerSession( - viewUrl: viewUrl, - capabilities: caps, - token: token, - expiresAt: expiresAt, - ); - } - - @override - Future saveAnnotations( - String orgId, - String fileId, - List annotations, - ) async { - await Future.delayed(const Duration(seconds: 2)); - // Mock: just delay, assume success - } -}