Fix Uri parsing errors in document viewer - viewUrl is already a Uri
This commit is contained in:
@@ -291,9 +291,8 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
label: const Text('Download File'),
|
label: const Text('Download File'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Open file download
|
// Open file download
|
||||||
final Uri url = Uri.parse(state.viewUrl);
|
|
||||||
// In a real implementation, you'd use url_launcher
|
// In a real implementation, you'd use url_launcher
|
||||||
// launchUrl(url);
|
// launchUrl(state.viewUrl);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -498,34 +497,174 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (state is DocumentViewerReady) {
|
if (state is DocumentViewerReady) {
|
||||||
if (state.caps.isPdf) {
|
|
||||||
return BlocBuilder<SessionBloc, SessionState>(
|
return BlocBuilder<SessionBloc, SessionState>(
|
||||||
builder: (context, sessionState) {
|
builder: (context, sessionState) {
|
||||||
String? token;
|
String? token;
|
||||||
if (sessionState is SessionActive) {
|
if (sessionState is SessionActive) {
|
||||||
token = sessionState.token;
|
token = sessionState.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.caps.isPdf) {
|
||||||
|
// PDF viewer using SfPdfViewer
|
||||||
return SfPdfViewer.network(
|
return SfPdfViewer.network(
|
||||||
state.viewUrl.toString(),
|
state.viewUrl.toString(),
|
||||||
headers: token != null
|
headers: token != null
|
||||||
? {'Authorization': 'Bearer $token'}
|
? {'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 {
|
} else if (state.caps.isText) {
|
||||||
// Placeholder for office docs iframe
|
// Text file viewer
|
||||||
return Container(
|
return FutureBuilder<String>(
|
||||||
color: AppTheme.secondaryText,
|
future: _fetchTextContent(
|
||||||
child: Center(
|
state.viewUrl.toString(),
|
||||||
child: Text(
|
token ?? '',
|
||||||
'Office Document Viewer\n(URL: ${state.viewUrl})',
|
),
|
||||||
textAlign: TextAlign.center,
|
builder: (context, snapshot) {
|
||||||
style: const TextStyle(color: AppTheme.primaryText),
|
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: 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'));
|
return const Center(child: Text('No document loaded'));
|
||||||
},
|
},
|
||||||
@@ -534,6 +673,23 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewerBloc.close();
|
_viewerBloc.close();
|
||||||
|
|||||||
@@ -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<String, List<FileItem>> _orgFiles = {};
|
|
||||||
final _uuid = const Uuid();
|
|
||||||
|
|
||||||
List<FileItem> _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<List<FileItem>> 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<FileItem?> 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<EditorSession> 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<void> deleteFile(String orgId, String path) async {
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
files.removeWhere((f) => f.path == path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> 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<void> 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<void> 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<List<FileItem>> 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<void> 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<ViewerSession> 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<void> saveAnnotations(
|
|
||||||
String orgId,
|
|
||||||
String fileId,
|
|
||||||
List<Annotation> annotations,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
|
||||||
// Mock: just delay, assume success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user