Add url_launcher dependency and implement hyperlink handling in document viewer
This commit is contained in:
@@ -14,6 +14,7 @@ import '../injection.dart';
|
|||||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
// Modal version for overlay display
|
// Modal version for overlay display
|
||||||
class DocumentViewerModal extends StatefulWidget {
|
class DocumentViewerModal extends StatefulWidget {
|
||||||
@@ -205,6 +206,8 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
headers: {'Authorization': 'Bearer ${state.token}'},
|
headers: {'Authorization': 'Bearer ${state.token}'},
|
||||||
onDocumentLoadFailed: (details) {},
|
onDocumentLoadFailed: (details) {},
|
||||||
onDocumentLoaded: (PdfDocumentLoadedDetails details) {},
|
onDocumentLoaded: (PdfDocumentLoadedDetails details) {},
|
||||||
|
onHyperlinkClicked: (details) =>
|
||||||
|
_handleHyperlink(details.uri),
|
||||||
);
|
);
|
||||||
} else if (state.caps.isImage) {
|
} else if (state.caps.isImage) {
|
||||||
// Image viewer
|
// Image viewer
|
||||||
@@ -523,6 +526,75 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
return _buildCollaboraIframe(proxyUrl);
|
return _buildCollaboraIframe(proxyUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleHyperlink(String url) async {
|
||||||
|
final shouldOpen = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Container(
|
||||||
|
decoration: AppTheme.glassDecoration,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Open Link',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Do you want to open this link in your browser?\n\n$url',
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(color: AppTheme.primaryText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text(
|
||||||
|
'Open',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: AppTheme.accentColor,
|
||||||
|
decorationThickness: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldOpen == true) {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewerBloc.close();
|
_viewerBloc.close();
|
||||||
@@ -700,6 +772,8 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
: {},
|
: {},
|
||||||
onDocumentLoadFailed: (details) {},
|
onDocumentLoadFailed: (details) {},
|
||||||
onDocumentLoaded: (PdfDocumentLoadedDetails details) {},
|
onDocumentLoaded: (PdfDocumentLoadedDetails details) {},
|
||||||
|
onHyperlinkClicked: (details) =>
|
||||||
|
_handleHyperlink(details.uri),
|
||||||
);
|
);
|
||||||
} else if (state.caps.isImage) {
|
} else if (state.caps.isImage) {
|
||||||
// Image viewer
|
// Image viewer
|
||||||
@@ -886,6 +960,75 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleHyperlink(String url) async {
|
||||||
|
final shouldOpen = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Container(
|
||||||
|
decoration: AppTheme.glassDecoration,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Open Link',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Do you want to open this link in your browser?\n\n$url',
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(color: AppTheme.primaryText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text(
|
||||||
|
'Open',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: AppTheme.accentColor,
|
||||||
|
decorationThickness: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldOpen == true) {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewerBloc.close();
|
_viewerBloc.close();
|
||||||
|
|||||||
@@ -122,8 +122,9 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
String _formatFileSize(int bytes) {
|
String _formatFileSize(int bytes) {
|
||||||
if (bytes < 1024) return '$bytes B';
|
if (bytes < 1024) return '$bytes B';
|
||||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
if (bytes < 1024 * 1024 * 1024)
|
if (bytes < 1024 * 1024 * 1024) {
|
||||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||||
|
}
|
||||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +366,10 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
void _createAndOpenDocument(String currentPath) async {
|
void _createAndOpenDocument(String currentPath) async {
|
||||||
final docName = await _showCreateDocumentDialog(context);
|
final docName = await _showCreateDocumentDialog(context);
|
||||||
if (docName == null || docName.isEmpty || !context.mounted) return;
|
if (docName == null || docName.isEmpty || !context.mounted) return;
|
||||||
|
if (mounted) {
|
||||||
// Show creating snackbar
|
// Show creating snackbar
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? snackController;
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>?
|
||||||
|
snackController;
|
||||||
snackController = ScaffoldMessenger.of(context).showSnackBar(
|
snackController = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
@@ -399,7 +401,8 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
final fileService = getIt<FileService>();
|
final fileService = getIt<FileService>();
|
||||||
// If orgId is 'personal', treat as empty string for personal workspace
|
// If orgId is 'personal', treat as empty string for personal workspace
|
||||||
// Always treat 'personal' or empty orgId as personal workspace
|
// Always treat 'personal' or empty orgId as personal workspace
|
||||||
final effectiveOrgId = (widget.orgId == 'personal' || widget.orgId.isEmpty)
|
final effectiveOrgId =
|
||||||
|
(widget.orgId == 'personal' || widget.orgId.isEmpty)
|
||||||
? ''
|
? ''
|
||||||
: widget.orgId;
|
: widget.orgId;
|
||||||
try {
|
try {
|
||||||
@@ -412,7 +415,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
snackController.close();
|
snackController.close();
|
||||||
|
|
||||||
// Refresh file list
|
// Refresh file list
|
||||||
if (context.mounted) {
|
if (mounted) {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
LoadDirectory(orgId: widget.orgId, path: currentPath),
|
LoadDirectory(orgId: widget.orgId, path: currentPath),
|
||||||
);
|
);
|
||||||
@@ -432,7 +435,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
errorMsg = data;
|
errorMsg = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (context.mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
@@ -454,6 +457,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showRenameDialog(FileItem file) async {
|
Future<void> _showRenameDialog(FileItem file) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
@@ -647,7 +651,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
|
|
||||||
// Dismiss preparing snackbar and show success
|
// Dismiss preparing snackbar and show success
|
||||||
snackController?.close();
|
snackController?.close();
|
||||||
if (context.mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
@@ -674,7 +678,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
snackController?.close();
|
snackController?.close();
|
||||||
if (context.mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
@@ -762,7 +766,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (confirmed == true) {
|
if (confirmed == true && mounted) {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
DeleteFile(orgId: widget.orgId, path: file.path),
|
DeleteFile(orgId: widget.orgId, path: file.path),
|
||||||
);
|
);
|
||||||
@@ -1438,7 +1442,9 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await FilePicker.platform
|
final result = await FilePicker.platform
|
||||||
.pickFiles(withData: true);
|
.pickFiles(withData: true);
|
||||||
if (result != null && result.files.isNotEmpty) {
|
if (result != null &&
|
||||||
|
result.files.isNotEmpty &&
|
||||||
|
context.mounted) {
|
||||||
final files = result.files
|
final files = result.files
|
||||||
.map(
|
.map(
|
||||||
(file) => FileItem(
|
(file) => FileItem(
|
||||||
@@ -1488,7 +1494,8 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
final folderName =
|
final folderName =
|
||||||
await _showCreateFolderDialog(context);
|
await _showCreateFolderDialog(context);
|
||||||
if (folderName != null &&
|
if (folderName != null &&
|
||||||
folderName.isNotEmpty) {
|
folderName.isNotEmpty &&
|
||||||
|
context.mounted) {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
CreateFolder(
|
CreateFolder(
|
||||||
orgId: widget.orgId,
|
orgId: widget.orgId,
|
||||||
|
|||||||
@@ -1334,7 +1334,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ dependencies:
|
|||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
connectivity_plus: ^7.0.0
|
connectivity_plus: ^7.0.0
|
||||||
provider: ^6.1.1
|
provider: ^6.1.1
|
||||||
|
url_launcher: ^6.2.2
|
||||||
file_picker: ^10.3.7
|
file_picker: ^10.3.7
|
||||||
flutter_dropzone: ^4.0.0
|
flutter_dropzone: ^4.0.0
|
||||||
desktop_drop: ^0.7.0
|
desktop_drop: ^0.7.0
|
||||||
|
|||||||
Reference in New Issue
Block a user