diff --git a/b0esche_cloud/lib/models/document_capabilities.dart b/b0esche_cloud/lib/models/document_capabilities.dart index 016295c..b3db140 100644 --- a/b0esche_cloud/lib/models/document_capabilities.dart +++ b/b0esche_cloud/lib/models/document_capabilities.dart @@ -4,21 +4,28 @@ class DocumentCapabilities extends Equatable { final bool canEdit; final bool canAnnotate; final bool isPdf; + final String mimeType; const DocumentCapabilities({ required this.canEdit, required this.canAnnotate, required this.isPdf, + required this.mimeType, }); @override - List get props => [canEdit, canAnnotate, isPdf]; + List get props => [canEdit, canAnnotate, isPdf, mimeType]; factory DocumentCapabilities.fromJson(Map json) { return DocumentCapabilities( canEdit: json['canEdit'], canAnnotate: json['canAnnotate'], isPdf: json['isPdf'], + mimeType: json['mimeType'] ?? 'application/octet-stream', ); } + + bool get isImage => mimeType.startsWith('image/'); + bool get isText => mimeType.startsWith('text/'); + bool get isOffice => mimeType.contains('word') || mimeType.contains('spreadsheet') || mimeType.contains('presentation'); } diff --git a/b0esche_cloud/lib/pages/document_viewer.dart b/b0esche_cloud/lib/pages/document_viewer.dart index 5dba01d..7249425 100644 --- a/b0esche_cloud/lib/pages/document_viewer.dart +++ b/b0esche_cloud/lib/pages/document_viewer.dart @@ -10,6 +10,7 @@ import '../services/file_service.dart'; import '../injection.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; // Modal version for overlay display class DocumentViewerModal extends StatefulWidget { @@ -177,24 +178,159 @@ class _DocumentViewerModalState extends State { ); } if (state is DocumentViewerReady) { + // Handle different file types based on MIME type if (state.caps.isPdf) { - // Log the URL and auth being used for debugging - - // Pass token via Authorization header - SfPdfViewer supports headers parameter + // PDF viewer using SfPdfViewer return SfPdfViewer.network( state.viewUrl.toString(), headers: {'Authorization': 'Bearer ${state.token}'}, onDocumentLoadFailed: (details) {}, onDocumentLoaded: (PdfDocumentLoadedDetails details) {}, ); - } else { + } else if (state.caps.isImage) { + // Image viewer return Container( - color: AppTheme.secondaryText, + color: AppTheme.primaryBackground, + child: InteractiveViewer( + minScale: 0.5, + maxScale: 4.0, + child: Image.network( + state.viewUrl.toString(), + headers: {'Authorization': 'Bearer ${state.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(), + state.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: Text( - 'Office Document Viewer\\n(URL: ${state.viewUrl})', - textAlign: TextAlign.center, - style: const TextStyle(color: AppTheme.primaryText), + 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 + final Uri url = Uri.parse(state.viewUrl); + // In a real implementation, you'd use url_launcher + // launchUrl(url); + }, + ), + ], + ), + ), + ); + } 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, + ), + ), + ], ), ), ); @@ -214,6 +350,23 @@ class _DocumentViewerModalState 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/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index ac91477..783a9c3 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -469,30 +469,33 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtM } downloadPath := fmt.Sprintf("%s://%s/orgs/%s/files/download?path=%s&token=%s", scheme, host, orgID.String(), url.QueryEscape(file.Path), url.QueryEscape(viewerToken)) - // Determine if it's a PDF based on file extension + // Determine file type based on extension isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf") + mimeType := getMimeType(file.Name) viewerSession := struct { ViewUrl string `json:"viewUrl"` Token string `json:"token"` Capabilities struct { - CanEdit bool `json:"canEdit"` - CanAnnotate bool `json:"canAnnotate"` - IsPdf bool `json:"isPdf"` + CanEdit bool `json:"canEdit"` + CanAnnotate bool `json:"canAnnotate"` + IsPdf bool `json:"isPdf"` + MimeType string `json:"mimeType"` } `json:"capabilities"` ExpiresAt string `json:"expiresAt"` }{ ViewUrl: downloadPath, Token: viewerToken, // Long-lived JWT token for authenticating file download Capabilities: struct { - CanEdit bool `json:"canEdit"` - CanAnnotate bool `json:"canAnnotate"` - IsPdf bool `json:"isPdf"` - }{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf}, + CanEdit bool `json:"canEdit"` + CanAnnotate bool `json:"canAnnotate"` + IsPdf bool `json:"isPdf"` + MimeType string `json:"mimeType"` + }{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf, MimeType: mimeType}, ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339), } - fmt.Printf("[VIEWER-SESSION] orgId=%s, fileId=%s, token_included=yes, isPdf=%v\n", orgID.String(), fileId, isPdf) + fmt.Printf("[VIEWER-SESSION] orgId=%s, fileId=%s, token_included=yes, isPdf=%v, mimeType=%s\n", orgID.String(), fileId, isPdf, mimeType) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(viewerSession) @@ -552,34 +555,38 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, } downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s&token=%s", scheme, host, url.QueryEscape(file.Path), url.QueryEscape(viewerToken)) - // Determine if it's a PDF based on file extension + // Determine file type based on extension isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf") + mimeType := getMimeType(file.Name) viewerSession := struct { ViewUrl string `json:"viewUrl"` Token string `json:"token"` Capabilities struct { - CanEdit bool `json:"canEdit"` - CanAnnotate bool `json:"canAnnotate"` - IsPdf bool `json:"isPdf"` + CanEdit bool `json:"canEdit"` + CanAnnotate bool `json:"canAnnotate"` + IsPdf bool `json:"isPdf"` + MimeType string `json:"mimeType"` } `json:"capabilities"` ExpiresAt string `json:"expiresAt"` }{ ViewUrl: downloadPath, Token: viewerToken, // Long-lived JWT token for authenticating file download Capabilities: struct { - CanEdit bool `json:"canEdit"` - CanAnnotate bool `json:"canAnnotate"` - IsPdf bool `json:"isPdf"` + CanEdit bool `json:"canEdit"` + CanAnnotate bool `json:"canAnnotate"` + IsPdf bool `json:"isPdf"` + MimeType string `json:"mimeType"` }{ CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf, + MimeType: mimeType, }, ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339), } - fmt.Printf("[VIEWER-SESSION] userId=%s, fileId=%s, token_included=yes, isPdf=%v\n", userID.String(), fileId, isPdf) + fmt.Printf("[VIEWER-SESSION] userId=%s, fileId=%s, token_included=yes, isPdf=%v, mimeType=%s\n", userID.String(), fileId, isPdf, mimeType) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(viewerSession) @@ -1765,3 +1772,38 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas io.Copy(w, resp.Body) } + +// getMimeType returns the MIME type based on file extension +func getMimeType(filename string) string { + lower := strings.ToLower(filename) + switch { + case strings.HasSuffix(lower, ".pdf"): + return "application/pdf" + case strings.HasSuffix(lower, ".png"): + return "image/png" + case strings.HasSuffix(lower, ".jpg"), strings.HasSuffix(lower, ".jpeg"): + return "image/jpeg" + case strings.HasSuffix(lower, ".gif"): + return "image/gif" + case strings.HasSuffix(lower, ".webp"): + return "image/webp" + case strings.HasSuffix(lower, ".svg"): + return "image/svg+xml" + case strings.HasSuffix(lower, ".txt"): + return "text/plain" + case strings.HasSuffix(lower, ".html"): + return "text/html" + case strings.HasSuffix(lower, ".json"): + return "application/json" + case strings.HasSuffix(lower, ".csv"): + return "text/csv" + case strings.HasSuffix(lower, ".doc"), strings.HasSuffix(lower, ".docx"): + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + case strings.HasSuffix(lower, ".xls"), strings.HasSuffix(lower, ".xlsx"): + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + case strings.HasSuffix(lower, ".ppt"), strings.HasSuffix(lower, ".pptx"): + return "application/vnd.openxmlformats-officedocument.presentationml.presentation" + default: + return "application/octet-stream" + } +}