Implement WOPI integration for Collabora Online
- Add WOPI models (CheckFileInfoResponse, PutFileResponse, LockInfo) - Implement WOPI handlers: CheckFileInfo, GetFile, PutFile, Lock operations - Add file locking mechanism to prevent concurrent editing conflicts - Add WOPI session endpoint for generating access tokens - Add UpdateFileSize method to database - Add WOPI routes (/wopi/files/* endpoints) - Update Flutter document viewer to load Collabora via WOPI WOPISrc URL - Implement WebView integration for Collabora Online viewer - Add comprehensive logging for WOPI operations [WOPI-TOKEN], [WOPI-REQUEST], [WOPI-STORAGE], [WOPI-LOCK]
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:convert';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/document_viewer/document_viewer_bloc.dart';
|
||||
@@ -11,6 +12,7 @@ import '../injection.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
// Modal version for overlay display
|
||||
class DocumentViewerModal extends StatefulWidget {
|
||||
@@ -325,68 +327,137 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
||||
}
|
||||
|
||||
Widget _buildCollaboraViewer(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,
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Collabora Online Viewer',
|
||||
style: TextStyle(
|
||||
color: AppTheme.secondaryText,
|
||||
fontSize: 14,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Opening document in Collabora...',
|
||||
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),
|
||||
),
|
||||
),
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final wopiSession = snapshot.data!;
|
||||
|
||||
// Build Collabora Online viewer URL with WOPISrc
|
||||
final collaboraUrl = 'https://of.b0esche.cloud/loleaflet/dist/loleaflet.html?WOPISrc=${Uri.encodeComponent(wopiSession.wopisrc)}';
|
||||
|
||||
// Use WebView to display Collabora Online
|
||||
return _buildWebView(collaboraUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<WOPISession> _createWOPISession(String token) async {
|
||||
try {
|
||||
final sessionBloc = BlocProvider.of<SessionBloc>(context);
|
||||
final baseUrl = (sessionBloc.state as SessionLoaded).baseUrl;
|
||||
|
||||
// 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 _buildWebView(String url) {
|
||||
final controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..loadRequest(Uri.parse(url));
|
||||
|
||||
return WebViewWidget(controller: controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewerBloc.close();
|
||||
@@ -394,6 +465,24 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user