Skip to content

Commit 2fe36e7

Browse files
committed
Merge branch 'main' of github.com:immich-app/immich into workflow-ui
2 parents 89360e7 + 1e1c4ac commit 2fe36e7

File tree

8 files changed

+116
-24
lines changed

8 files changed

+116
-24
lines changed

mobile/lib/constants/constants.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const double kUploadStatusCanceled = -2.0;
5050

5151
const int kMinMonthsToEnableScrubberSnap = 12;
5252

53-
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id6449244941";
53+
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652";
5454
const String kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich";
5555
const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest";
5656

mobile/lib/infrastructure/repositories/timeline.repository.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
265265
row.deletedAt.isNull() &
266266
row.isFavorite.equals(true) &
267267
row.ownerId.equals(userId) &
268-
row.visibility.equalsValue(AssetVisibility.timeline),
268+
(row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
269269
groupBy: groupBy,
270270
origin: TimelineOrigin.favorite,
271271
);

mobile/lib/presentation/pages/drift_memory.page.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ class DriftMemoryPage extends HookConsumerWidget {
2424

2525
const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key});
2626

27+
static void setMemory(WidgetRef ref, DriftMemory memory) {
28+
if (memory.assets.isNotEmpty) {
29+
ref.read(currentAssetNotifier.notifier).setAsset(memory.assets.first);
30+
31+
if (memory.assets.first.isVideo) {
32+
ref.read(videoPlaybackValueProvider.notifier).reset();
33+
}
34+
}
35+
}
36+
2737
@override
2838
Widget build(BuildContext context, WidgetRef ref) {
2939
final currentMemory = useState(memories[memoryIndex]);
@@ -202,6 +212,10 @@ class DriftMemoryPage extends HookConsumerWidget {
202212
if (pageNumber < memories.length) {
203213
currentMemoryIndex.value = pageNumber;
204214
currentMemory.value = memories[pageNumber];
215+
216+
WidgetsBinding.instance.addPostFrameCallback((_) {
217+
DriftMemoryPage.setMemory(ref, memories[pageNumber]);
218+
});
205219
}
206220

207221
currentAssetPage.value = 0;

mobile/lib/presentation/widgets/memory/memory_lane.widget.dart

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
33
import 'package:hooks_riverpod/hooks_riverpod.dart';
44
import 'package:immich_mobile/domain/models/memory.model.dart';
55
import 'package:immich_mobile/extensions/translate_extensions.dart';
6+
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
67
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
7-
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
88
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
9-
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
109
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
1110
import 'package:immich_mobile/routing/router.dart';
1211

@@ -31,16 +30,9 @@ class DriftMemoryLane extends ConsumerWidget {
3130
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
3231
onTap: (index) {
3332
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
34-
3533
if (memories[index].assets.isNotEmpty) {
36-
final asset = memories[index].assets[0];
37-
ref.read(currentAssetNotifier.notifier).setAsset(asset);
38-
39-
if (asset.isVideo) {
40-
ref.read(videoPlaybackValueProvider.notifier).reset();
41-
}
34+
DriftMemoryPage.setMemory(ref, memories[index]);
4235
}
43-
4436
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
4537
},
4638
children: memories

pnpm-lock.yaml

Lines changed: 17 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@
3131
"@immich/ui": "^0.45.1",
3232
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
3333
"@mdi/js": "^7.4.47",
34-
"@photo-sphere-viewer/core": "^5.11.5",
35-
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
36-
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
37-
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
38-
"@photo-sphere-viewer/video-plugin": "^5.11.5",
34+
"@photo-sphere-viewer/core": "^5.14.0",
35+
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
36+
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
37+
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
38+
"@photo-sphere-viewer/settings-plugin": "^5.14.0",
39+
"@photo-sphere-viewer/video-plugin": "^5.14.0",
3940
"@types/geojson": "^7946.0.16",
4041
"@zoom-image/core": "^0.41.0",
4142
"@zoom-image/svelte": "^0.3.0",

web/src/lib/actions/shortcut.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,17 @@ export const shortcutLabel = (shortcut: Shortcut) => {
3939
/** Determines whether an event should be ignored. The event will be ignored if:
4040
* - The element dispatching the event is not the same as the element which the event listener is attached to
4141
* - The element dispatching the event is an input field
42+
* - The element dispatching the event is a map canvas
4243
*/
4344
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
4445
if (event.target === event.currentTarget) {
4546
return false;
4647
}
4748
const type = (event.target as HTMLInputElement).type;
48-
return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type);
49+
return (
50+
['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) ||
51+
(event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas'))
52+
);
4953
};
5054

5155
export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {

web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
23
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
34
import {
45
EquirectangularAdapter,
@@ -8,11 +9,21 @@
89
type PluginConstructor,
910
} from '@photo-sphere-viewer/core';
1011
import '@photo-sphere-viewer/core/index.css';
12+
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
13+
import '@photo-sphere-viewer/markers-plugin/index.css';
1114
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
1215
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
1316
import '@photo-sphere-viewer/settings-plugin/index.css';
1417
import { onDestroy, onMount } from 'svelte';
1518
19+
// Adapted as well as possible from classlist 'border-solid border-white border-3 rounded-lg'
20+
const FACE_BOX_SVG_STYLE = {
21+
fill: 'rgba(0, 0, 0, 0)',
22+
stroke: '#ffffff',
23+
strokeWidth: '3px',
24+
strokeLinejoin: 'round',
25+
};
26+
1627
interface Props {
1728
panorama: string | { source: string };
1829
originalPanorama?: string | { source: string };
@@ -26,6 +37,62 @@
2637
let container: HTMLDivElement | undefined = $state();
2738
let viewer: Viewer;
2839
40+
let animationInProgress: { cancel: () => void } | undefined;
41+
let previousFaces: Faces[] = [];
42+
43+
const boundingBoxesUnsubscribe = boundingBoxesArray.subscribe((faces: Faces[]) => {
44+
// Debounce; don't do anything when the data didn't actually change.
45+
if (faces === previousFaces) {
46+
return;
47+
}
48+
previousFaces = faces;
49+
50+
if (animationInProgress) {
51+
animationInProgress.cancel();
52+
animationInProgress = undefined;
53+
}
54+
if (!viewer || !viewer.state.textureData || !viewer.getPlugin(MarkersPlugin)) {
55+
return;
56+
}
57+
const markersPlugin = viewer.getPlugin<MarkersPlugin>(MarkersPlugin);
58+
59+
// croppedWidth is the size of the texture, which might be cropped to be less than 360/180 degrees.
60+
// This is what we want because the facial recognition is done on the image, not the sphere.
61+
const currentTextureWidth = viewer.state.textureData.panoData.croppedWidth;
62+
63+
markersPlugin.clearMarkers();
64+
for (const [index, face] of faces.entries()) {
65+
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2 } = face;
66+
const ratio = currentTextureWidth / face.imageWidth;
67+
// Pixel values are translated to spherical coordinates and only then added to the panorama;
68+
// no need to recalculate when the texture image changes to the original size.
69+
markersPlugin.addMarker({
70+
id: `face_${index}`,
71+
polygonPixels: [
72+
[x1 * ratio, y1 * ratio],
73+
[x2 * ratio, y1 * ratio],
74+
[x2 * ratio, y2 * ratio],
75+
[x1 * ratio, y2 * ratio],
76+
],
77+
svgStyle: FACE_BOX_SVG_STYLE,
78+
});
79+
}
80+
81+
// Smoothly pan to the highlighted (hovered-over) face.
82+
if (faces.length === 1) {
83+
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2, imageWidth: w } = faces[0];
84+
const ratio = currentTextureWidth / w;
85+
const x = ((x1 + x2) * ratio) / 2;
86+
const y = ((y1 + y2) * ratio) / 2;
87+
animationInProgress = viewer.animate({
88+
textureX: x,
89+
textureY: y,
90+
zoom: Math.min(viewer.getZoomLevel(), 75),
91+
speed: 500, // duration in ms
92+
});
93+
}
94+
});
95+
2996
onMount(() => {
3097
if (!container) {
3198
return;
@@ -34,6 +101,7 @@
34101
viewer = new Viewer({
35102
adapter,
36103
plugins: [
104+
MarkersPlugin,
37105
SettingsPlugin,
38106
[
39107
ResolutionPlugin,
@@ -68,7 +136,7 @@
68136
zoomSpeed: 0.5,
69137
fisheye: false,
70138
});
71-
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
139+
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
72140
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
73141
// zoomLevel range: [0, 100]
74142
if (Math.round(zoomLevel) >= 75) {
@@ -89,6 +157,7 @@
89157
if (viewer) {
90158
viewer.destroy();
91159
}
160+
boundingBoxesUnsubscribe();
92161
});
93162
</script>
94163

0 commit comments

Comments
 (0)