mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 01:11:36 +03:00
Merge branch 'main' of github.com:immich-app/immich into workflow-ui
This commit is contained in:
@@ -50,7 +50,7 @@ const double kUploadStatusCanceled = -2.0;
|
|||||||
|
|
||||||
const int kMinMonthsToEnableScrubberSnap = 12;
|
const int kMinMonthsToEnableScrubberSnap = 12;
|
||||||
|
|
||||||
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id6449244941";
|
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652";
|
||||||
const String kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich";
|
const String kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich";
|
||||||
const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest";
|
const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest";
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.isFavorite.equals(true) &
|
row.isFavorite.equals(true) &
|
||||||
row.ownerId.equals(userId) &
|
row.ownerId.equals(userId) &
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline),
|
(row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
origin: TimelineOrigin.favorite,
|
origin: TimelineOrigin.favorite,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ class DriftMemoryPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key});
|
const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key});
|
||||||
|
|
||||||
|
static void setMemory(WidgetRef ref, DriftMemory memory) {
|
||||||
|
if (memory.assets.isNotEmpty) {
|
||||||
|
ref.read(currentAssetNotifier.notifier).setAsset(memory.assets.first);
|
||||||
|
|
||||||
|
if (memory.assets.first.isVideo) {
|
||||||
|
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final currentMemory = useState(memories[memoryIndex]);
|
final currentMemory = useState(memories[memoryIndex]);
|
||||||
@@ -202,6 +212,10 @@ class DriftMemoryPage extends HookConsumerWidget {
|
|||||||
if (pageNumber < memories.length) {
|
if (pageNumber < memories.length) {
|
||||||
currentMemoryIndex.value = pageNumber;
|
currentMemoryIndex.value = pageNumber;
|
||||||
currentMemory.value = memories[pageNumber];
|
currentMemory.value = memories[pageNumber];
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
DriftMemoryPage.setMemory(ref, memories[pageNumber]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAssetPage.value = 0;
|
currentAssetPage.value = 0;
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@@ -31,16 +30,9 @@ class DriftMemoryLane extends ConsumerWidget {
|
|||||||
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
|
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
|
||||||
if (memories[index].assets.isNotEmpty) {
|
if (memories[index].assets.isNotEmpty) {
|
||||||
final asset = memories[index].assets[0];
|
DriftMemoryPage.setMemory(ref, memories[index]);
|
||||||
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
|
||||||
|
|
||||||
if (asset.isVideo) {
|
|
||||||
ref.read(videoPlaybackValueProvider.notifier).reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
|
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
|
||||||
},
|
},
|
||||||
children: memories
|
children: memories
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -726,19 +726,22 @@ importers:
|
|||||||
specifier: ^7.4.47
|
specifier: ^7.4.47
|
||||||
version: 7.4.47
|
version: 7.4.47
|
||||||
'@photo-sphere-viewer/core':
|
'@photo-sphere-viewer/core':
|
||||||
specifier: ^5.11.5
|
specifier: ^5.14.0
|
||||||
version: 5.14.0
|
version: 5.14.0
|
||||||
'@photo-sphere-viewer/equirectangular-video-adapter':
|
'@photo-sphere-viewer/equirectangular-video-adapter':
|
||||||
specifier: ^5.11.5
|
specifier: ^5.14.0
|
||||||
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))
|
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))
|
||||||
|
'@photo-sphere-viewer/markers-plugin':
|
||||||
|
specifier: ^5.14.0
|
||||||
|
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
||||||
'@photo-sphere-viewer/resolution-plugin':
|
'@photo-sphere-viewer/resolution-plugin':
|
||||||
specifier: ^5.11.5
|
specifier: ^5.14.0
|
||||||
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))
|
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))
|
||||||
'@photo-sphere-viewer/settings-plugin':
|
'@photo-sphere-viewer/settings-plugin':
|
||||||
specifier: ^5.11.5
|
specifier: ^5.14.0
|
||||||
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
||||||
'@photo-sphere-viewer/video-plugin':
|
'@photo-sphere-viewer/video-plugin':
|
||||||
specifier: ^5.11.5
|
specifier: ^5.14.0
|
||||||
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
||||||
'@types/geojson':
|
'@types/geojson':
|
||||||
specifier: ^7946.0.16
|
specifier: ^7946.0.16
|
||||||
@@ -3725,6 +3728,11 @@ packages:
|
|||||||
'@photo-sphere-viewer/core': 5.14.0
|
'@photo-sphere-viewer/core': 5.14.0
|
||||||
'@photo-sphere-viewer/video-plugin': 5.14.0
|
'@photo-sphere-viewer/video-plugin': 5.14.0
|
||||||
|
|
||||||
|
'@photo-sphere-viewer/markers-plugin@5.14.0':
|
||||||
|
resolution: {integrity: sha512-w7txVHtLxXMS61m0EbNjgvdNXQYRh6Aa0oatft5oruKgoXLg/UlCu1mG6Btg+zrNsG05W2zl4gRM3fcWoVdneA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@photo-sphere-viewer/core': 5.14.0
|
||||||
|
|
||||||
'@photo-sphere-viewer/resolution-plugin@5.14.0':
|
'@photo-sphere-viewer/resolution-plugin@5.14.0':
|
||||||
resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==}
|
resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -15769,6 +15777,10 @@ snapshots:
|
|||||||
'@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
'@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0)
|
||||||
three: 0.180.0
|
three: 0.180.0
|
||||||
|
|
||||||
|
'@photo-sphere-viewer/markers-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)':
|
||||||
|
dependencies:
|
||||||
|
'@photo-sphere-viewer/core': 5.14.0
|
||||||
|
|
||||||
'@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))':
|
'@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@photo-sphere-viewer/core': 5.14.0
|
'@photo-sphere-viewer/core': 5.14.0
|
||||||
|
|||||||
@@ -31,11 +31,12 @@
|
|||||||
"@immich/ui": "^0.45.1",
|
"@immich/ui": "^0.45.1",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
||||||
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
|
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
||||||
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
|
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
||||||
"@photo-sphere-viewer/video-plugin": "^5.11.5",
|
"@photo-sphere-viewer/settings-plugin": "^5.14.0",
|
||||||
|
"@photo-sphere-viewer/video-plugin": "^5.14.0",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@zoom-image/core": "^0.41.0",
|
"@zoom-image/core": "^0.41.0",
|
||||||
"@zoom-image/svelte": "^0.3.0",
|
"@zoom-image/svelte": "^0.3.0",
|
||||||
|
|||||||
@@ -39,13 +39,17 @@ export const shortcutLabel = (shortcut: Shortcut) => {
|
|||||||
/** Determines whether an event should be ignored. The event will be ignored if:
|
/** Determines whether an event should be ignored. The event will be ignored if:
|
||||||
* - The element dispatching the event is not the same as the element which the event listener is attached to
|
* - The element dispatching the event is not the same as the element which the event listener is attached to
|
||||||
* - The element dispatching the event is an input field
|
* - The element dispatching the event is an input field
|
||||||
|
* - The element dispatching the event is a map canvas
|
||||||
*/
|
*/
|
||||||
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
|
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
|
||||||
if (event.target === event.currentTarget) {
|
if (event.target === event.currentTarget) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const type = (event.target as HTMLInputElement).type;
|
const type = (event.target as HTMLInputElement).type;
|
||||||
return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type);
|
return (
|
||||||
|
['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) ||
|
||||||
|
(event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas'))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
|
export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||||
import {
|
import {
|
||||||
EquirectangularAdapter,
|
EquirectangularAdapter,
|
||||||
@@ -8,11 +9,21 @@
|
|||||||
type PluginConstructor,
|
type PluginConstructor,
|
||||||
} from '@photo-sphere-viewer/core';
|
} from '@photo-sphere-viewer/core';
|
||||||
import '@photo-sphere-viewer/core/index.css';
|
import '@photo-sphere-viewer/core/index.css';
|
||||||
|
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
|
||||||
|
import '@photo-sphere-viewer/markers-plugin/index.css';
|
||||||
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
||||||
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
|
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
|
||||||
import '@photo-sphere-viewer/settings-plugin/index.css';
|
import '@photo-sphere-viewer/settings-plugin/index.css';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
|
// Adapted as well as possible from classlist 'border-solid border-white border-3 rounded-lg'
|
||||||
|
const FACE_BOX_SVG_STYLE = {
|
||||||
|
fill: 'rgba(0, 0, 0, 0)',
|
||||||
|
stroke: '#ffffff',
|
||||||
|
strokeWidth: '3px',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panorama: string | { source: string };
|
panorama: string | { source: string };
|
||||||
originalPanorama?: string | { source: string };
|
originalPanorama?: string | { source: string };
|
||||||
@@ -26,6 +37,62 @@
|
|||||||
let container: HTMLDivElement | undefined = $state();
|
let container: HTMLDivElement | undefined = $state();
|
||||||
let viewer: Viewer;
|
let viewer: Viewer;
|
||||||
|
|
||||||
|
let animationInProgress: { cancel: () => void } | undefined;
|
||||||
|
let previousFaces: Faces[] = [];
|
||||||
|
|
||||||
|
const boundingBoxesUnsubscribe = boundingBoxesArray.subscribe((faces: Faces[]) => {
|
||||||
|
// Debounce; don't do anything when the data didn't actually change.
|
||||||
|
if (faces === previousFaces) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previousFaces = faces;
|
||||||
|
|
||||||
|
if (animationInProgress) {
|
||||||
|
animationInProgress.cancel();
|
||||||
|
animationInProgress = undefined;
|
||||||
|
}
|
||||||
|
if (!viewer || !viewer.state.textureData || !viewer.getPlugin(MarkersPlugin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const markersPlugin = viewer.getPlugin<MarkersPlugin>(MarkersPlugin);
|
||||||
|
|
||||||
|
// croppedWidth is the size of the texture, which might be cropped to be less than 360/180 degrees.
|
||||||
|
// This is what we want because the facial recognition is done on the image, not the sphere.
|
||||||
|
const currentTextureWidth = viewer.state.textureData.panoData.croppedWidth;
|
||||||
|
|
||||||
|
markersPlugin.clearMarkers();
|
||||||
|
for (const [index, face] of faces.entries()) {
|
||||||
|
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2 } = face;
|
||||||
|
const ratio = currentTextureWidth / face.imageWidth;
|
||||||
|
// Pixel values are translated to spherical coordinates and only then added to the panorama;
|
||||||
|
// no need to recalculate when the texture image changes to the original size.
|
||||||
|
markersPlugin.addMarker({
|
||||||
|
id: `face_${index}`,
|
||||||
|
polygonPixels: [
|
||||||
|
[x1 * ratio, y1 * ratio],
|
||||||
|
[x2 * ratio, y1 * ratio],
|
||||||
|
[x2 * ratio, y2 * ratio],
|
||||||
|
[x1 * ratio, y2 * ratio],
|
||||||
|
],
|
||||||
|
svgStyle: FACE_BOX_SVG_STYLE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly pan to the highlighted (hovered-over) face.
|
||||||
|
if (faces.length === 1) {
|
||||||
|
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2, imageWidth: w } = faces[0];
|
||||||
|
const ratio = currentTextureWidth / w;
|
||||||
|
const x = ((x1 + x2) * ratio) / 2;
|
||||||
|
const y = ((y1 + y2) * ratio) / 2;
|
||||||
|
animationInProgress = viewer.animate({
|
||||||
|
textureX: x,
|
||||||
|
textureY: y,
|
||||||
|
zoom: Math.min(viewer.getZoomLevel(), 75),
|
||||||
|
speed: 500, // duration in ms
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
@@ -34,6 +101,7 @@
|
|||||||
viewer = new Viewer({
|
viewer = new Viewer({
|
||||||
adapter,
|
adapter,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
MarkersPlugin,
|
||||||
SettingsPlugin,
|
SettingsPlugin,
|
||||||
[
|
[
|
||||||
ResolutionPlugin,
|
ResolutionPlugin,
|
||||||
@@ -68,7 +136,7 @@
|
|||||||
zoomSpeed: 0.5,
|
zoomSpeed: 0.5,
|
||||||
fisheye: false,
|
fisheye: false,
|
||||||
});
|
});
|
||||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||||
// zoomLevel range: [0, 100]
|
// zoomLevel range: [0, 100]
|
||||||
if (Math.round(zoomLevel) >= 75) {
|
if (Math.round(zoomLevel) >= 75) {
|
||||||
@@ -89,6 +157,7 @@
|
|||||||
if (viewer) {
|
if (viewer) {
|
||||||
viewer.destroy();
|
viewer.destroy();
|
||||||
}
|
}
|
||||||
|
boundingBoxesUnsubscribe();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user