From 68eada7d7cc0140d2c07f3934885af4c8630da2c Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 21:52:04 +0100 Subject: [PATCH 1/6] feat(mobile): open in browser --- i18n/en.json | 1 + .../open_in_browser_action_button.widget.dart | 42 +++++++++++++++++++ mobile/lib/utils/action_button.utils.dart | 9 ++++ 3 files changed, 52 insertions(+) create mode 100644 mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart diff --git a/i18n/en.json b/i18n/en.json index 95e958403283a..02496b928d8f5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1650,6 +1650,7 @@ "only_favorites": "Only favorites", "open": "Open", "open_calendar": "Open calendar", + "open_in_browser": "Open in browser", "open_in_map_view": "Open in map view", "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart new file mode 100644 index 0000000000000..0ac5b03dace49 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class OpenInBrowserActionButton extends ConsumerWidget { + final String remoteId; + final bool iconOnly; + final bool menuItem; + final Color? iconColor; + + const OpenInBrowserActionButton({ + super.key, + required this.remoteId, + this.iconOnly = false, + this.menuItem = false, + this.iconColor, + }); + + void _onTap() async { + final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); + final url = '$serverEndpoint/photos/$remoteId'; + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + label: 'open_in_browser'.t(context: context), + iconData: Icons.open_in_browser, + iconColor: iconColor, + iconOnly: iconOnly, + menuItem: menuItem, + onPressed: _onTap, + ); + } +} diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index dccb7657605ea..f6c14f0e7508c 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -70,6 +71,7 @@ enum ActionButtonType { download, upload, unstack, + openInBrowser, archive, unarchive, moveToLockFolder, @@ -138,6 +140,7 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.isStacked, + ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView && !context.isArchived, ActionButtonType.likeActivity => !context.isInLockedView && context.currentAlbum != null && @@ -215,6 +218,12 @@ enum ActionButtonType { ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.openInBrowser => OpenInBrowserActionButton( + remoteId: context.asset.remoteId!, + iconOnly: iconOnly, + menuItem: menuItem, + iconColor: context.originalTheme?.iconTheme.color, + ), ActionButtonType.similarPhotos => SimilarPhotosActionButton( assetId: (context.asset as RemoteAsset).id, iconOnly: iconOnly, From 955555024c438f10ca94f3febe5672133af50b3e Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 21:55:27 +0100 Subject: [PATCH 2/6] chore: open in browser instead of webview --- .../action_buttons/open_in_browser_action_button.widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart index 0ac5b03dace49..f9b0ec3734737 100644 --- a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart @@ -24,7 +24,7 @@ class OpenInBrowserActionButton extends ConsumerWidget { final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); final url = '$serverEndpoint/photos/$remoteId'; if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } } From fb14bcb9de925a7e20205844eb6ddcc2faef6a21 Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 21:59:28 +0100 Subject: [PATCH 3/6] chore: allow archived asset --- mobile/lib/utils/action_button.utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index f6c14f0e7508c..f70d2fa26f581 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -140,7 +140,7 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.isStacked, - ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView && !context.isArchived, + ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView, ActionButtonType.likeActivity => !context.isInLockedView && context.currentAlbum != null && From 334b9f17e91d5995b6d52a7db152f6676b8cde71 Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 20 Feb 2026 09:55:38 +0100 Subject: [PATCH 4/6] fix: moved openinbrowser above unstack --- mobile/lib/utils/action_button.utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index f70d2fa26f581..ebf8b79204f7d 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -70,8 +70,8 @@ enum ActionButtonType { viewInTimeline, download, upload, - unstack, openInBrowser, + unstack, archive, unarchive, moveToLockFolder, From c4edc517e04332226803064e865bcd4dd39e7000 Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 20 Feb 2026 10:13:11 +0100 Subject: [PATCH 5/6] feat: deeplink into favorites, trash & archived --- .../open_in_browser_action_button.widget.dart | 21 ++++++++++++++++++- mobile/lib/utils/action_button.utils.dart | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart index f9b0ec3734737..17703d0beb2fe 100644 --- a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; @@ -8,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; class OpenInBrowserActionButton extends ConsumerWidget { final String remoteId; + final TimelineOrigin origin; final bool iconOnly; final bool menuItem; final Color? iconColor; @@ -15,6 +17,7 @@ class OpenInBrowserActionButton extends ConsumerWidget { const OpenInBrowserActionButton({ super.key, required this.remoteId, + required this.origin, this.iconOnly = false, this.menuItem = false, this.iconColor, @@ -22,7 +25,23 @@ class OpenInBrowserActionButton extends ConsumerWidget { void _onTap() async { final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); - final url = '$serverEndpoint/photos/$remoteId'; + + String originPath = ''; + switch (origin) { + case TimelineOrigin.favorite: + originPath = '/favorites'; + break; + case TimelineOrigin.trash: + originPath = '/trash'; + break; + case TimelineOrigin.archive: + originPath = '/archive'; + break; + default: + break; + } + + final url = '$serverEndpoint$originPath/photos/$remoteId'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index ebf8b79204f7d..8702f2fd8b64e 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -219,7 +219,8 @@ enum ActionButtonType { ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.openInBrowser => OpenInBrowserActionButton( - remoteId: context.asset.remoteId!, + remoteId: (context.asset as RemoteAsset).id, + origin: context.timelineOrigin, iconOnly: iconOnly, menuItem: menuItem, iconColor: context.originalTheme?.iconTheme.color, From df5384f8d9addb2954518229f03ba62f5d521720 Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 20 Feb 2026 11:45:31 +0100 Subject: [PATCH 6/6] fix: use remoteId (for tests to succeed) --- mobile/lib/utils/action_button.utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 8702f2fd8b64e..8aa52b941d117 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -219,7 +219,7 @@ enum ActionButtonType { ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.openInBrowser => OpenInBrowserActionButton( - remoteId: (context.asset as RemoteAsset).id, + remoteId: context.asset.remoteId!, origin: context.timelineOrigin, iconOnly: iconOnly, menuItem: menuItem,