- Remove URL building with query parameters that was causing GET requests - Pass WOPISrc directly to _buildCollaboraIframe function - JavaScript now creates form with POST method and WOPISrc in body - This ensures Collabora receives POST not GET request
880 lines
31 KiB
Dart
880 lines
31 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:convert';
|
|
import 'dart:html' as html;
|
|
import 'dart:ui_web' as ui;
|
|
import '../theme/app_theme.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../blocs/document_viewer/document_viewer_bloc.dart';
|
|
import '../blocs/document_viewer/document_viewer_event.dart';
|
|
import '../blocs/document_viewer/document_viewer_state.dart';
|
|
import '../blocs/session/session_bloc.dart';
|
|
import '../blocs/session/session_state.dart';
|
|
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 {
|
|
final String orgId;
|
|
final String fileId;
|
|
final VoidCallback onClose;
|
|
final VoidCallback onEdit;
|
|
|
|
const DocumentViewerModal({
|
|
super.key,
|
|
required this.orgId,
|
|
required this.fileId,
|
|
required this.onClose,
|
|
required this.onEdit,
|
|
});
|
|
|
|
@override
|
|
State<DocumentViewerModal> createState() => _DocumentViewerModalState();
|
|
}
|
|
|
|
class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|
late DocumentViewerBloc _viewerBloc;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_viewerBloc = DocumentViewerBloc(getIt<FileService>());
|
|
_viewerBloc.add(DocumentOpened(orgId: widget.orgId, fileId: widget.fileId));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider.value(
|
|
value: _viewerBloc,
|
|
child: Column(
|
|
children: [
|
|
// Custom AppBar
|
|
Container(
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryBackground.withValues(alpha: 0.9),
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: AppTheme.accentColor.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 16),
|
|
const Text(
|
|
'Document Viewer',
|
|
style: TextStyle(
|
|
color: AppTheme.primaryText,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerReady && state.caps.canEdit) {
|
|
return IconButton(
|
|
icon: const Icon(
|
|
Icons.edit,
|
|
color: AppTheme.primaryText,
|
|
),
|
|
onPressed: widget.onEdit,
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh, color: AppTheme.primaryText),
|
|
splashColor: Colors.transparent,
|
|
highlightColor: Colors.transparent,
|
|
onPressed: () {
|
|
_viewerBloc.add(DocumentReloaded());
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: AppTheme.primaryText),
|
|
splashColor: Colors.transparent,
|
|
highlightColor: Colors.transparent,
|
|
onPressed: () {
|
|
_viewerBloc.add(DocumentClosed());
|
|
widget.onClose();
|
|
},
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
),
|
|
),
|
|
// Meta info bar
|
|
BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerReady) {
|
|
return Container(
|
|
height: 30,
|
|
alignment: Alignment.centerLeft,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryBackground.withValues(alpha: 0.3),
|
|
),
|
|
child: const Text(
|
|
'Last modified: Unknown by Unknown (v1)',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.secondaryText,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
// Document content
|
|
Expanded(
|
|
child: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerLoading) {
|
|
return Container(
|
|
color: AppTheme.primaryBackground,
|
|
child: const Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
AppTheme.accentColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (state is DocumentViewerError) {
|
|
return Center(
|
|
child: Text(
|
|
'Error: ${state.message}',
|
|
style: const TextStyle(color: AppTheme.primaryText),
|
|
),
|
|
);
|
|
}
|
|
if (state is DocumentViewerSessionExpired) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text(
|
|
'Your viewing session expired. Click to reopen.',
|
|
style: TextStyle(color: AppTheme.primaryText),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_viewerBloc.add(
|
|
DocumentOpened(
|
|
orgId: widget.orgId,
|
|
fileId: widget.fileId,
|
|
),
|
|
);
|
|
},
|
|
child: const Text('Reload'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
if (state is DocumentViewerReady) {
|
|
// Handle different file types based on MIME type
|
|
if (state.caps.isPdf) {
|
|
// PDF viewer using SfPdfViewer
|
|
return SfPdfViewer.network(
|
|
state.viewUrl.toString(),
|
|
headers: {'Authorization': 'Bearer ${state.token}'},
|
|
onDocumentLoadFailed: (details) {},
|
|
onDocumentLoaded: (PdfDocumentLoadedDetails details) {},
|
|
);
|
|
} 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: {'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 using Collabora Online
|
|
return _buildCollaboraViewer(
|
|
state.viewUrl.toString(),
|
|
state.token,
|
|
);
|
|
} 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',
|
|
style: TextStyle(color: AppTheme.primaryText),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
Widget _buildCollaboraViewer(String documentUrl, String token) {
|
|
// Create WOPI session to get WOPISrc URL
|
|
return FutureBuilder<WOPISession>(
|
|
future: _createWOPISession(token),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Container(
|
|
color: AppTheme.primaryBackground,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
AppTheme.accentColor,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Loading document in Collabora Online...',
|
|
style: TextStyle(
|
|
color: AppTheme.secondaryText,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (snapshot.hasError) {
|
|
return Container(
|
|
color: AppTheme.primaryBackground,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Failed to load document',
|
|
style: TextStyle(
|
|
color: AppTheme.primaryText,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${snapshot.error}',
|
|
style: TextStyle(
|
|
color: AppTheme.secondaryText,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!snapshot.hasData) {
|
|
return Container(
|
|
color: AppTheme.primaryBackground,
|
|
child: const Center(
|
|
child: Text(
|
|
'No session data',
|
|
style: TextStyle(color: AppTheme.primaryText),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final wopiSession = snapshot.data!;
|
|
|
|
// Don't build URL with query parameters - pass WOPISrc separately to JavaScript
|
|
return _buildWebView(wopiSession.wopisrc);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<WOPISession> _createWOPISession(String token) async {
|
|
try {
|
|
// Use default base URL from backend
|
|
String baseUrl = 'https://go.b0esche.cloud';
|
|
|
|
// Determine endpoint based on whether we're in org or user workspace
|
|
String endpoint;
|
|
if (widget.orgId.isNotEmpty && widget.orgId != 'personal') {
|
|
endpoint = '/orgs/${widget.orgId}/files/${widget.fileId}/wopi-session';
|
|
} else {
|
|
endpoint = '/user/files/${widget.fileId}/wopi-session';
|
|
}
|
|
|
|
final response = await http
|
|
.post(
|
|
Uri.parse('$baseUrl$endpoint'),
|
|
headers: {
|
|
'Authorization': 'Bearer $token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (response.statusCode == 200) {
|
|
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
|
return WOPISession(
|
|
wopisrc: json['wopi_src'] as String,
|
|
accessToken: json['access_token'] as String,
|
|
);
|
|
} else {
|
|
throw Exception(
|
|
'Failed to create WOPI session: ${response.statusCode}',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
throw Exception('Error creating WOPI session: $e');
|
|
}
|
|
}
|
|
|
|
Widget _buildCollaboraIframe(String wopisrc) {
|
|
// For Collabora Online, we need to POST the WOPISrc, not GET it
|
|
// Use JavaScript to create and submit the form properly
|
|
final String viewType = 'collabora-form-${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
ui.platformViewRegistry.registerViewFactory(viewType, (int viewId) {
|
|
// Create a container for the form and iframe
|
|
final container = html.DivElement()
|
|
..style.width = '100%'
|
|
..style.height = '100%'
|
|
..style.margin = '0'
|
|
..style.padding = '0'
|
|
..style.overflow = 'hidden';
|
|
|
|
// Create the iframe first
|
|
final iframe = html.IFrameElement()
|
|
..name = 'collabora-iframe-$viewId'
|
|
..style.border = 'none'
|
|
..style.width = '100%'
|
|
..style.height = '100%'
|
|
..style.margin = '0'
|
|
..style.padding = '0'
|
|
..setAttribute(
|
|
'allow',
|
|
'microphone; camera; usb; autoplay; clipboard-read; clipboard-write',
|
|
)
|
|
..setAttribute(
|
|
'sandbox',
|
|
'allow-scripts allow-popups allow-forms allow-pointer-lock allow-presentation allow-modals allow-downloads allow-popups-to-escape-sandbox',
|
|
);
|
|
|
|
container.append(iframe);
|
|
|
|
// Use JavaScript to create and submit the form
|
|
// This is more reliable than Dart's form.submit()
|
|
final jsCode = '''
|
|
(function() {
|
|
var form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = 'https://of.b0esche.cloud/loleaflet/dist/loleaflet.html';
|
|
form.target = 'collabora-iframe-$viewId';
|
|
form.style.display = 'none';
|
|
|
|
var input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'WOPISrc';
|
|
input.value = '$wopisrc';
|
|
|
|
form.appendChild(input);
|
|
document.body.appendChild(form);
|
|
|
|
// Submit the form
|
|
setTimeout(function() {
|
|
form.submit();
|
|
}, 50);
|
|
})();
|
|
''';
|
|
|
|
final script = html.ScriptElement()
|
|
..type = 'text/javascript'
|
|
..text = jsCode;
|
|
|
|
html.document.body!.append(script);
|
|
|
|
return container;
|
|
});
|
|
|
|
return HtmlElementView(viewType: viewType);
|
|
}
|
|
|
|
Widget _buildWebView(String wopisrc) {
|
|
// Embed Collabora Online in an iframe for web platform
|
|
return _buildCollaboraIframe(wopisrc);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_viewerBloc.close();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
// WOPI Session model for Collabora Online integration
|
|
class WOPISession {
|
|
final String wopisrc;
|
|
final String accessToken;
|
|
|
|
WOPISession({required this.wopisrc, required this.accessToken});
|
|
|
|
factory WOPISession.fromJson(Map<String, dynamic> json) {
|
|
return WOPISession(
|
|
wopisrc: json['wopi_src'] as String,
|
|
accessToken: json['access_token'] as String,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Original page version (for routing if needed)
|
|
class DocumentViewer extends StatefulWidget {
|
|
final String orgId;
|
|
final String fileId;
|
|
|
|
const DocumentViewer({super.key, required this.orgId, required this.fileId});
|
|
|
|
@override
|
|
State<DocumentViewer> createState() => _DocumentViewerState();
|
|
}
|
|
|
|
class _DocumentViewerState extends State<DocumentViewer> {
|
|
late DocumentViewerBloc _viewerBloc;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_viewerBloc = DocumentViewerBloc(getIt<FileService>());
|
|
_viewerBloc.add(DocumentOpened(orgId: widget.orgId, fileId: widget.fileId));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider.value(
|
|
value: _viewerBloc,
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Document Viewer'),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(30),
|
|
child: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerReady) {
|
|
// Placeholder for meta
|
|
return Container(
|
|
height: 30,
|
|
alignment: Alignment.centerLeft,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'Last modified: Unknown by Unknown (v1)',
|
|
style: const TextStyle(fontSize: 12),
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
actions: [
|
|
BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerReady && state.caps.canEdit) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.edit),
|
|
onPressed: () {
|
|
GoRouter.of(
|
|
context,
|
|
).go('/editor/${widget.orgId}/${widget.fileId}');
|
|
},
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
splashColor: Colors.transparent,
|
|
highlightColor: Colors.transparent,
|
|
onPressed: () {
|
|
_viewerBloc.add(DocumentReloaded());
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
splashColor: Colors.transparent,
|
|
highlightColor: Colors.transparent,
|
|
onPressed: () {
|
|
_viewerBloc.add(DocumentClosed());
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
|
builder: (context, state) {
|
|
if (state is DocumentViewerLoading) {
|
|
return Container(
|
|
color: AppTheme.primaryBackground,
|
|
child: const Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
AppTheme.accentColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (state is DocumentViewerError) {
|
|
return Center(child: Text('Error: ${state.message}'));
|
|
}
|
|
if (state is DocumentViewerSessionExpired) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text(
|
|
'Your viewing session expired. Click to reopen.',
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_viewerBloc.add(
|
|
DocumentOpened(
|
|
orgId: widget.orgId,
|
|
fileId: widget.fileId,
|
|
),
|
|
);
|
|
},
|
|
child: const Text('Reload'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
if (state is DocumentViewerReady) {
|
|
return BlocBuilder<SessionBloc, SessionState>(
|
|
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 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<String>(
|
|
future: _fetchTextContent(
|
|
state.viewUrl.toString(),
|
|
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 using Collabora Online
|
|
return _buildCollaboraViewerPage(
|
|
state.viewUrl.toString(),
|
|
token ?? '',
|
|
);
|
|
} 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'));
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
Widget _buildCollaboraViewerPage(String documentUrl, String token) {
|
|
// Build HTML to embed Collabora Online viewer
|
|
// For now, we'll show the document download option with a link to open in Collabora
|
|
// A proper implementation would require WOPI protocol support
|
|
|
|
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(
|
|
'Office Document',
|
|
style: TextStyle(
|
|
color: AppTheme.primaryText,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Collabora Online Viewer',
|
|
style: TextStyle(color: AppTheme.secondaryText, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Opening document in Collabora...',
|
|
style: TextStyle(color: AppTheme.secondaryText, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 24),
|
|
CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentColor),
|
|
),
|
|
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);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_viewerBloc.close();
|
|
super.dispose();
|
|
}
|
|
}
|