mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 11:20:12 +03:00
Compare commits
1 Commits
feat/pg-qu
...
shared-dee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44b4239a4f |
@@ -2358,6 +2358,7 @@
|
|||||||
"view_qr_code": "View QR code",
|
"view_qr_code": "View QR code",
|
||||||
"view_similar_photos": "View similar photos",
|
"view_similar_photos": "View similar photos",
|
||||||
"view_stack": "View Stack",
|
"view_stack": "View Stack",
|
||||||
|
"view_in_app": "View in the Immich app",
|
||||||
"view_user": "View User",
|
"view_user": "View User",
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/photos/" />
|
android:pathPrefix="/photos/" />
|
||||||
|
<data
|
||||||
|
android:host="my.immich.app"
|
||||||
|
android:pathPrefix="/share/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/services/album.service.dart';
|
|||||||
import 'package:immich_mobile/services/asset.service.dart';
|
import 'package:immich_mobile/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/services/memory.service.dart';
|
import 'package:immich_mobile/services/memory.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final deepLinkServiceProvider = Provider(
|
final deepLinkServiceProvider = Provider(
|
||||||
(ref) => DeepLinkService(
|
(ref) => DeepLinkService(
|
||||||
@@ -102,10 +103,13 @@ class DeepLinkService {
|
|||||||
|
|
||||||
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
|
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
|
||||||
final path = link.uri.path;
|
final path = link.uri.path;
|
||||||
|
final queryParams = link.uri.queryParameters;
|
||||||
|
|
||||||
const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
|
const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
|
||||||
final assetRegex = RegExp('/photos/($uuidRegex)');
|
final assetRegex = RegExp('/photos/($uuidRegex)');
|
||||||
final albumRegex = RegExp('/albums/($uuidRegex)');
|
final albumRegex = RegExp('/albums/($uuidRegex)');
|
||||||
|
// Share links can use UUID keys or custom slugs
|
||||||
|
final shareRegex = RegExp(r'/share/([^/?]+)');
|
||||||
|
|
||||||
PageRouteInfo<dynamic>? deepLinkRoute;
|
PageRouteInfo<dynamic>? deepLinkRoute;
|
||||||
if (assetRegex.hasMatch(path)) {
|
if (assetRegex.hasMatch(path)) {
|
||||||
@@ -116,6 +120,19 @@ class DeepLinkService {
|
|||||||
deepLinkRoute = await _buildAlbumDeepLink(albumId);
|
deepLinkRoute = await _buildAlbumDeepLink(albumId);
|
||||||
} else if (path == "/memory") {
|
} else if (path == "/memory") {
|
||||||
deepLinkRoute = await _buildMemoryDeepLink(null);
|
deepLinkRoute = await _buildMemoryDeepLink(null);
|
||||||
|
} else if (shareRegex.hasMatch(path)) {
|
||||||
|
// Handle shared links by opening them in the browser
|
||||||
|
// The mobile app doesn't have a native viewer for external shared links yet
|
||||||
|
final serverUrl = queryParams['server'];
|
||||||
|
final shareKey = shareRegex.firstMatch(path)?.group(1);
|
||||||
|
if (serverUrl != null && shareKey != null) {
|
||||||
|
final decodedServerUrl = Uri.decodeComponent(serverUrl);
|
||||||
|
final shareUrl = Uri.parse('$decodedServerUrl/share/$shareKey');
|
||||||
|
await launchUrl(shareUrl, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
// Return appropriate deep link based on app state
|
||||||
|
if (isColdStart) return DeepLink.defaultPath;
|
||||||
|
return DeepLink.none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep link resolution failed, safely handle it based on the app state
|
// Deep link resolution failed, safely handle it based on the app state
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
|
import OpenInAppBanner from '$lib/components/shared-components/open-in-app-banner.svelte';
|
||||||
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
@@ -65,6 +67,14 @@
|
|||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
{#if key}
|
||||||
|
<meta
|
||||||
|
name="apple-itunes-app"
|
||||||
|
content="app-id=1613945652, app-argument=https://my.immich.app/share/{key}?server={encodeURIComponent(
|
||||||
|
globalThis.location?.origin ?? ''
|
||||||
|
)}"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
{#if passwordRequired}
|
{#if passwordRequired}
|
||||||
<main
|
<main
|
||||||
@@ -106,3 +116,7 @@
|
|||||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if key && browser}
|
||||||
|
<OpenInAppBanner shareKey={key} serverUrl={globalThis.location?.origin ?? ''} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { mdiClose } from '@mdi/js';
|
||||||
|
import { Button, Icon, Logo } from '@immich/ui';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
shareKey: string;
|
||||||
|
serverUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { shareKey, serverUrl }: Props = $props();
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'immich-open-in-app-dismissed';
|
||||||
|
const isMobile = $derived(
|
||||||
|
browser && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
|
||||||
|
);
|
||||||
|
|
||||||
|
let isDismissed = $state(browser && localStorage.getItem(STORAGE_KEY) === 'true');
|
||||||
|
let showBanner = $derived(isMobile && !isDismissed);
|
||||||
|
|
||||||
|
const deepLinkUrl = $derived(
|
||||||
|
`https://my.immich.app/share/${shareKey}?server=${encodeURIComponent(serverUrl)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
isDismissed = true;
|
||||||
|
if (browser) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showBanner}
|
||||||
|
<div
|
||||||
|
class="fixed bottom-0 left-0 right-0 z-50 flex items-center justify-between gap-3 bg-immich-bg dark:bg-immich-dark-gray p-3 shadow-lg border-t border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<div class="shrink-0 w-10 h-10">
|
||||||
|
<Logo variant="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="font-semibold text-immich-primary dark:text-immich-dark-primary text-sm truncate">
|
||||||
|
Immich
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
|
{$t('view_in_app')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<Button href={deepLinkUrl} size="small" shape="round">
|
||||||
|
{$t('open')}
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={dismiss}
|
||||||
|
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
aria-label={$t('close')}
|
||||||
|
>
|
||||||
|
<Icon icon={mdiClose} size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user