Multi-format document viewer: add image/text/office file type support
This commit is contained in:
@@ -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<Object?> get props => [canEdit, canAnnotate, isPdf];
|
||||
List<Object?> get props => [canEdit, canAnnotate, isPdf, mimeType];
|
||||
|
||||
factory DocumentCapabilities.fromJson(Map<String, dynamic> 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');
|
||||
}
|
||||
|
||||
@@ -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<DocumentViewerModal> {
|
||||
);
|
||||
}
|
||||
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<String>(
|
||||
future: _fetchTextContent(
|
||||
state.viewUrl.toString(),
|
||||
state.token,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
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<DocumentViewerModal> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _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();
|
||||
|
||||
Reference in New Issue
Block a user