Skip to content

Commit

Permalink
fix: unable to open local file using afLaunchUrl function (AppFlowy-I…
Browse files Browse the repository at this point in the history
…O#6927)

* fix: unable to open local file using afLaunchUrl function

* chore: use the latest api to open the local file

* chore: use the latest api to open the local file

* chore: use the latest api to open the local file

* test: add local paht regex test
  • Loading branch information
LucasXu0 authored Dec 6, 2024
1 parent 67fe0d6 commit 9e82f3d
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 51 deletions.
79 changes: 74 additions & 5 deletions frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'dart:io';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:open_filex/open_filex.dart';
import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;

typedef OnFailureCallback = void Function(Uri uri);

Future<bool> afLaunchUrl(
/// Launch the uri
///
/// If the uri is a local file path, it will be opened with the OpenFilex.
/// Otherwise, it will be launched with the url_launcher.
Future<bool> afLaunchUri(
Uri uri, {
BuildContext? context,
OnFailureCallback? onFailure,
launcher.LaunchMode mode = launcher.LaunchMode.platformDefault,
String? webOnlyWindowName,
bool addingHttpSchemeWhenFailed = false,
}) async {
final url = uri.toString();
final decodedUrl = Uri.decodeComponent(url);

// check if the uri is the local file path
if (localPathRegex.hasMatch(decodedUrl)) {
return _afLaunchLocalUri(
uri,
context: context,
onFailure: onFailure,
);
}

// try to launch the uri directly
bool result;
try {
Expand All @@ -32,7 +52,7 @@ Future<bool> afLaunchUrl(
}

// if the uri is not a valid url, try to launch it with http scheme
final url = uri.toString();

if (addingHttpSchemeWhenFailed &&
!result &&
!isURL(url, {'require_protocol': true})) {
Expand All @@ -54,9 +74,14 @@ Future<bool> afLaunchUrl(
return result;
}

/// Launch the url string
///
/// See [afLaunchUri] for more details.
Future<bool> afLaunchUrlString(
String url, {
bool addingHttpSchemeWhenFailed = false,
BuildContext? context,
OnFailureCallback? onFailure,
}) async {
final Uri uri;
try {
Expand All @@ -67,12 +92,56 @@ Future<bool> afLaunchUrlString(
}

// try to launch the uri directly
return afLaunchUrl(
return afLaunchUri(
uri,
addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed,
context: context,
onFailure: onFailure,
);
}

/// Launch the local uri
///
/// See [afLaunchUri] for more details.
Future<bool> _afLaunchLocalUri(
Uri uri, {
BuildContext? context,
OnFailureCallback? onFailure,
}) async {
final decodedUrl = Uri.decodeComponent(uri.toString());
// open the file with the OpenfileX
var result = await OpenFilex.open(decodedUrl);
if (result.type != ResultType.done) {
// For the file cant be opened, fallback to open the folder
final parentFolder = Directory(decodedUrl).parent.path;
result = await OpenFilex.open(parentFolder);
}
// show the toast if the file is not found
final message = switch (result.type) {
ResultType.done => LocaleKeys.openFileMessage_success.tr(),
ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(),
ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(),
ResultType.permissionDenied =>
LocaleKeys.openFileMessage_permissionDenied.tr(),
ResultType.error => LocaleKeys.failedToOpenUrl.tr(),
};
if (context != null && context.mounted) {
showToastNotification(
context,
message: message,
type: result.type == ResultType.done
? ToastificationType.success
: ToastificationType.error,
);
}
final openFileSuccess = result.type == ResultType.done;
if (!openFileSuccess && onFailure != null) {
onFailure(uri);
Log.error('Failed to open file: $result.message');
}
return openFileSuccess;
}

void _errorHandler(
Uri uri, {
BuildContext? context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
final url = context.read<ShareBloc>().state.url;
if (url.isNotEmpty) {
unawaited(
afLaunchUrl(
afLaunchUri(
Uri.parse(url),
mode: LaunchMode.externalApplication,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:flutter/material.dart';

import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';

Expand Down Expand Up @@ -159,7 +158,7 @@ class GridMediaCellSkin extends IEditableMediaCellSkin {
List<MediaFilePB> files,
) {
if (file.fileType != MediaFileTypePB.Image) {
afLaunchUrlString(file.url);
afLaunchUrlString(file.url, context: context);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:open_filex/open_filex.dart';
import 'package:reorderables/reorderables.dart';

const _dropFileKey = 'files_media';
Expand Down Expand Up @@ -479,7 +478,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
? null
: () {
if (file.uploadType == FileUploadTypePB.LocalFile) {
OpenFilex.open(file.url);
afLaunchUrlString(file.url);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:cross_file/cross_file.dart';
import 'package:desktop_drop/desktop_drop.dart';
Expand All @@ -19,7 +18,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:open_filex/open_filex.dart';
import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
Expand Down Expand Up @@ -323,22 +321,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
FileUrlType urlType,
String url,
) async {
if ([FileUrlType.cloud, FileUrlType.network].contains(urlType)) {
await afLaunchUrlString(url);
} else {
final result = await OpenFilex.open(url);
if (result.type == ResultType.done) {
return;
}

if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.document_plugins_file_failedToOpenMsg.tr(),
type: ToastificationType.error,
);
}
}
await afLaunchUrlString(url, context: context);
}

void _openMenu() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';

import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
Expand All @@ -22,6 +20,7 @@ import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:universal_platform/universal_platform.dart';
Expand Down Expand Up @@ -102,7 +101,7 @@ Future<void> downloadMediaFile(
FileUploadTypePB.LocalFile,
].contains(file.uploadType)) {
/// When the file is a network file or a local file, we can directly open the file.
await afLaunchUrl(Uri.parse(file.url));
await afLaunchUrlString(file.url);
} else {
if (userProfile == null) {
return showToastNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ final appflowySharePageLinkRegex = RegExp(appflowySharePageLinkPattern);

const _numberedListPattern = r'^(\d+)\.';
final numberedListRegex = RegExp(_numberedListPattern);

const _localPathPattern = r'^(file:\/\/|\/|\\|[a-zA-Z]:[/\\]|\.{1,2}[/\\])';
final localPathRegex = RegExp(_localPathPattern, caseSensitive: false);
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AppFlowyCloudAuthService implements AuthService {
(data) async {
// Open the webview with oauth url
final uri = Uri.parse(data.oauthUrl);
final isSuccess = await afLaunchUrl(
final isSuccess = await afLaunchUri(
uri,
mode: LaunchMode.externalApplication,
webOnlyWindowName: '_self',
Expand Down
4 changes: 2 additions & 2 deletions frontend/appflowy_flutter/lib/util/share_log_files.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dart:io';

import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:archive/archive_io.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
Expand Down Expand Up @@ -67,7 +67,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
await zipFile.delete();
} else {
// open the directory
await OpenFilex.open(zipFile.path);
await afLaunchUri(zipFile.uri);
}
} catch (e) {
if (context != null && context.mounted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher.dart' show launchUrl;

part 'plugin_state_bloc.freezed.dart';

class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
Expand Down Expand Up @@ -91,7 +92,7 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
final result = await AIEventGetModelStorageDirectory().send();
result.fold(
(data) {
afLaunchUrl(Uri.file(data.filePath));
afLaunchUri(Uri.file(data.filePath));
},
(err) => Log.error(err.toString()),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
Expand All @@ -28,6 +25,8 @@ import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';

Expand Down Expand Up @@ -450,7 +449,7 @@ class _DataPathActions extends StatelessWidget {
label:
LocaleKeys.settings_manageDataPage_dataStorage_actions_open.tr(),
icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)),
onPressed: () => afLaunchUrl(Uri.file(currentPath)),
onPressed: () => afLaunchUri(Uri.file(currentPath)),
),
],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';

import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';

class ThemeUploadLearnMoreButton extends StatelessWidget {
const ThemeUploadLearnMoreButton({super.key});
Expand All @@ -32,7 +31,7 @@ class ThemeUploadLearnMoreButton extends StatelessWidget {
),
onPressed: () async {
final uri = Uri.parse(learnMoreURL);
await afLaunchUrl(
await afLaunchUri(
uri,
context: context,
onFailure: (_) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,12 @@ class InteractiveImageToolbar extends StatelessWidget {
Future<void> _locateOrDownloadImage(BuildContext context) async {
if (currentImage.isLocal) {
/// If the image type is local, we simply open the image
await afLaunchUrl(Uri.file(currentImage.url));
await afLaunchUri(Uri.file(currentImage.url));
} else if (currentImage.isNotInternal) {
// In case of eg. Unsplash images (images without extension type in URL),
// we don't know their mimetype. In the future we can write a parser
// using the Mime package and read the image to get the proper extension.
await afLaunchUrl(Uri.parse(currentImage.url));
await afLaunchUri(Uri.parse(currentImage.url));
} else {
if (userProfile == null) {
return showSnapBar(
Expand Down
Loading

0 comments on commit 9e82f3d

Please sign in to comment.