Merge branch 'immich-app:main' into feat-web-set-asset-as-profile-image

This commit is contained in:
faupau
2023-07-06 20:48:35 +02:00
committed by GitHub
103 changed files with 22119 additions and 221 deletions

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.65.0
* The version of the OpenAPI document: 1.66.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -1772,11 +1772,17 @@ export interface PersonResponseDto {
*/
export interface PersonUpdateDto {
/**
*
* Person name.
* @type {string}
* @memberof PersonUpdateDto
*/
'name': string;
'name'?: string;
/**
* Asset is used to get the feature face thumbnail.
* @type {string}
* @memberof PersonUpdateDto
*/
'featureFaceAssetId'?: string;
}
/**
*

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.65.0
* The version of the OpenAPI document: 1.66.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.65.0
* The version of the OpenAPI document: 1.66.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.65.0
* The version of the OpenAPI document: 1.66.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.65.0
* The version of the OpenAPI document: 1.66.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -3,14 +3,15 @@
import { timeToSeconds } from '$lib/utils/time-to-seconds';
import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
import { createEventDispatcher } from 'svelte';
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import Heart from 'svelte-material-icons/Heart.svelte';
import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
import Heart from 'svelte-material-icons/Heart.svelte';
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
import { fade } from 'svelte/transition';
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
const dispatch = createEventDispatcher();
@@ -21,6 +22,7 @@
export let thumbnailHeight: number | undefined = undefined;
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
export let selected = false;
export let selectionCandidate = false;
export let disabled = false;
export let readonly = false;
export let publicSharedKey: string | undefined = undefined;
@@ -30,7 +32,7 @@
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
$: [width, height] = (() => {
$: [width, height] = ((): [number, number] => {
if (thumbnailSize) {
return [thumbnailSize, thumbnailSize];
}
@@ -42,12 +44,19 @@
return [235, 235];
})();
const thumbnailClickedHandler = () => {
const thumbnailClickedHandler = (e: Event) => {
if (!disabled) {
e.preventDefault();
dispatch('click', { asset });
}
};
const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
thumbnailClickedHandler(e);
}
};
const onIconClickedHandler = (e: MouseEvent) => {
e.stopPropagation();
if (!disabled) {
@@ -68,17 +77,17 @@
on:mouseenter={() => (mouseOver = true)}
on:mouseleave={() => (mouseOver = false)}
on:click={thumbnailClickedHandler}
on:keydown={thumbnailClickedHandler}
on:keydown={thumbnailKeyDownHandler}
>
{#if intersecting}
<div class="absolute w-full h-full z-20">
<!-- Select asset button -->
{#if !readonly}
{#if !readonly && (mouseOver || selected || selectionCandidate)}
<button
on:click={onIconClickedHandler}
class="absolute p-2 group-hover:block"
class:group-hover:block={!disabled}
class:hidden={!selected}
on:keydown|preventDefault
on:keyup|preventDefault
class="absolute p-2"
class:cursor-not-allowed={disabled}
role="checkbox"
aria-checked={selected}
@@ -153,6 +162,13 @@
</div>
{/if}
</div>
{#if selectionCandidate}
<div
class="absolute w-full h-full top-0 bg-immich-primary opacity-40"
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
/>
{/if}
{/if}
</div>
</IntersectionObserver>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import type { AssetResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import AssetSelectionViewer from '../shared-components/gallery-viewer/asset-selection-viewer.svelte';
const dispatch = createEventDispatcher();
export let assets: AssetResponseDto[];
let selectedAsset: AssetResponseDto | undefined = undefined;
const handleSelectedAsset = async (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
selectedAsset = asset;
onClose();
};
const onClose = () => {
dispatch('go-back', { selectedAsset });
};
</script>
<section
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute top-0 left-0 w-full h-full bg-immich-bg dark:bg-immich-dark-bg z-[9999]"
>
<ControlAppBar on:close-button-click={onClose}>
<svelte:fragment slot="leading">Select feature photo</svelte:fragment>
</ControlAppBar>
<section class="pt-[100px] pl-[70px] bg-immich-bg dark:bg-immich-dark-bg">
<AssetSelectionViewer {assets} on:select={handleSelectedAsset} />
</section>
</section>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import {
assetInteractionStore,
assetSelectionCandidates,
assetsInAlbumStoreState,
isMultiSelectStoreState,
selectedAssets,
@@ -11,12 +12,13 @@
import type { AssetResponseDto } from '@api';
import justifiedLayout from 'justified-layout';
import lodash from 'lodash-es';
import { createEventDispatcher } from 'svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
import { fly } from 'svelte/transition';
import { DateTime, Interval } from 'luxon';
import { getAssetRatio } from '$lib/utils/asset-utils';
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
import { createEventDispatcher } from 'svelte';
export let assets: AssetResponseDto[];
export let bucketDate: string;
@@ -130,18 +132,19 @@
dateGroupTitle: string,
) => {
if ($selectedAssets.has(asset)) {
for (const candidate of $assetSelectionCandidates || []) {
assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
}
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
} else {
for (const candidate of $assetSelectionCandidates || []) {
assetInteractionStore.addAssetToMultiselectGroup(candidate);
}
assetInteractionStore.addAssetToMultiselectGroup(asset);
}
// Check if all assets are selected in a group to toggle the group selection's icon
let selectedAssetsInGroupCount = 0;
assetsInDateGroup.forEach((asset) => {
if ($selectedAssets.has(asset)) {
selectedAssetsInGroupCount++;
}
});
let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
// if all assets are selected in a group, add the group to selected group
if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
@@ -151,9 +154,48 @@
}
};
const assetMouseEventHandler = (dateGroupTitle: string) => {
const assetMouseEventHandler = (dateGroupTitle: string, asset: AssetResponseDto | null) => {
// Show multi select icon on hover on date group
hoveredDateGroup = dateGroupTitle;
if ($isMultiSelectStoreState) {
dispatch('selectAssetCandidates', { asset });
}
};
const formatGroupTitle = (date: DateTime): string => {
const today = DateTime.now().startOf('day');
// Today
if (today.hasSame(date, 'day')) {
return 'Today';
}
// Yesterday
if (Interval.fromDateTimes(date, today).length('days') == 1) {
return 'Yesterday';
}
// Last week
if (Interval.fromDateTimes(date, today).length('weeks') < 1) {
return date.toLocaleString({ weekday: 'long' });
}
// This year
if (today.hasSame(date, 'year')) {
return date.toLocaleString({
weekday: 'short',
month: 'short',
day: 'numeric',
});
}
return date.toLocaleString({
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
});
};
</script>
@@ -164,16 +206,19 @@
bind:clientWidth={viewportWidth}
>
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString($locale, groupDateFormat)}
{@const dateGroupTitle = formatGroupTitle(DateTime.fromISO(assetsInDateGroup[0].fileCreatedAt).startOf('day'))}
<!-- Asset Group By Date -->
<div
class="flex flex-col mt-5"
on:mouseenter={() => {
isMouseOverGroup = true;
assetMouseEventHandler(dateGroupTitle);
assetMouseEventHandler(dateGroupTitle, null);
}}
on:mouseleave={() => {
isMouseOverGroup = false;
assetMouseEventHandler(dateGroupTitle, null);
}}
on:mouseleave={() => (isMouseOverGroup = false)}
>
<!-- Date group title -->
<p
@@ -195,7 +240,7 @@
</div>
{/if}
<span class="truncate" title={dateGroupTitle}>
<span class="first-letter:capitalize truncate" title={dateGroupTitle}>
{dateGroupTitle}
</span>
</p>
@@ -216,9 +261,10 @@
{groupIndex}
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle, asset)}
selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
selectionCandidate={$assetSelectionCandidates.has(asset)}
disabled={$assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
thumbnailWidth={box.width}
thumbnailHeight={box.height}
/>

View File

@@ -1,12 +1,15 @@
<script lang="ts">
import { BucketPosition } from '$lib/models/asset-grid-state';
import {
assetInteractionStore,
isMultiSelectStoreState,
isViewingAssetStoreState,
selectedAssets,
viewingAssetStoreState,
} from '$lib/stores/asset-interaction.store';
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
import type { UserResponseDto } from '@api';
import { AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum, api } from '@api';
import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum } from '@api';
import { onDestroy, onMount } from 'svelte';
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
@@ -16,7 +19,6 @@
OnScrollbarDragDetail,
} from '../shared-components/scrollbar/scrollbar.svelte';
import AssetDateGroup from './asset-date-group.svelte';
import { BucketPosition } from '$lib/models/asset-grid-state';
import MemoryLane from './memory-lane.svelte';
export let user: UserResponseDto | undefined = undefined;
@@ -111,8 +113,80 @@
navigateToNextAsset();
assetStore.removeAsset(asset.id);
};
let lastAssetMouseEvent: AssetResponseDto | null = null;
$: if (!lastAssetMouseEvent) {
assetInteractionStore.clearAssetSelectionCandidates();
}
let shiftKeyIsDown = false;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
e.preventDefault();
shiftKeyIsDown = true;
}
};
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
e.preventDefault();
shiftKeyIsDown = false;
}
};
$: if (!shiftKeyIsDown) {
assetInteractionStore.clearAssetSelectionCandidates();
}
$: if (shiftKeyIsDown && lastAssetMouseEvent) {
selectAssetCandidates(lastAssetMouseEvent);
}
const getLastSelectedAsset = () => {
let value;
for (value of $selectedAssets);
return value;
};
const handleSelectAssetCandidates = (e: CustomEvent) => {
const asset = e.detail.asset;
if (asset) {
selectAssetCandidates(asset);
}
lastAssetMouseEvent = asset;
};
const selectAssetCandidates = (asset: AssetResponseDto) => {
if (!shiftKeyIsDown) {
return;
}
const lastSelectedAsset = getLastSelectedAsset();
if (!lastSelectedAsset) {
return;
}
let start = $assetGridState.assets.indexOf(asset);
let end = $assetGridState.assets.indexOf(lastSelectedAsset);
if (start > end) {
[start, end] = [end, start];
}
assetInteractionStore.setAssetSelectionCandidates($assetGridState.assets.slice(start, end + 1));
};
const onSelectStart = (e: Event) => {
if ($isMultiSelectStoreState && shiftKeyIsDown) {
e.preventDefault();
}
};
</script>
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} />
{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
<Scrollbar
scrollbarHeight={viewportHeight}
@@ -155,6 +229,7 @@
<AssetDateGroup
{isAlbumSelectionMode}
on:shift={handleScrollTimeline}
on:selectAssetCandidates={handleSelectAssetCandidates}
assets={bucket.assets}
bucketDate={bucket.bucketDate}
bucketHeight={bucket.bucketHeight}

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
import { AssetResponseDto, ThumbnailFormat } from '@api';
import { createEventDispatcher } from 'svelte';
import { flip } from 'svelte/animate';
export let assets: AssetResponseDto[];
export let selectedAssets: Set<AssetResponseDto> = new Set();
let viewWidth: number;
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
let dispatch = createEventDispatcher();
const selectAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
let temp = new Set(selectedAssets);
if (selectedAssets.has(asset)) {
temp.delete(asset);
} else {
temp.add(asset);
}
selectedAssets = temp;
dispatch('select', { asset, selectedAssets });
};
</script>
{#if assets.length > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each assets as asset (asset.id)}
<div animate:flip={{ duration: 500 }}>
<Thumbnail
{asset}
{thumbnailSize}
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
on:click={selectAssetHandler}
selected={selectedAssets.has(asset)}
/>
</div>
{/each}
</div>
{/if}

View File

@@ -10,6 +10,7 @@
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
import { flip } from 'svelte/animate';
import { archivedAsset } from '$lib/stores/archived-asset.store';
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
export let assets: AssetResponseDto[];
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
@@ -24,22 +25,10 @@
let currentViewAssetIndex = 0;
let viewWidth: number;
let thumbnailSize = 300;
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
$: isMultiSelectionMode = selectedAssets.size > 0;
$: {
if (assets.length < 6) {
thumbnailSize = Math.min(320, Math.floor(viewWidth / assets.length - assets.length));
} else {
if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 7 - 7);
else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
}
}
const viewAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;

View File

@@ -46,6 +46,11 @@ export class AssetGridState {
*/
assets: AssetResponseDto[] = [];
/**
* Total assets that have been loaded along with additional data
*/
loadedAssets: Record<string, number> = {};
/**
* User that owns assets
*/

View File

@@ -1,6 +1,5 @@
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
import { api, AssetResponseDto } from '@api';
import { sortBy } from 'lodash-es';
import { derived, writable } from 'svelte/store';
import { assetGridState, assetStore } from './assets.store';
@@ -13,6 +12,7 @@ export const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]);
export const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
export const selectedGroup = writable<Set<string>>(new Set());
export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
export const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set());
function createAssetInteractionStore() {
let _assetGridState = new AssetGridState();
@@ -20,8 +20,7 @@ function createAssetInteractionStore() {
let _selectedAssets: Set<AssetResponseDto>;
let _selectedGroup: Set<string>;
let _assetsInAlbums: AssetResponseDto[];
let savedAssetLength = 0;
let assetSortedByDate: AssetResponseDto[] = [];
let _assetSelectionCandidates: Set<AssetResponseDto>;
// Subscriber
assetGridState.subscribe((state) => {
@@ -44,6 +43,10 @@ function createAssetInteractionStore() {
_assetsInAlbums = assets;
});
assetSelectionCandidates.subscribe((assets) => {
_assetSelectionCandidates = assets;
});
// Methods
/**
@@ -63,41 +66,64 @@ function createAssetInteractionStore() {
isViewingAssetStoreState.set(isViewing);
};
const navigateAsset = async (direction: 'next' | 'previous') => {
// Flatten and sort the asset by date if there are new assets
if (assetSortedByDate.length === 0 || savedAssetLength !== _assetGridState.assets.length) {
assetSortedByDate = sortBy(_assetGridState.assets, (a) => a.fileCreatedAt);
savedAssetLength = _assetGridState.assets.length;
const getNextAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
// Find the index of the current asset
const currentIndex = assetSortedByDate.findIndex((a) => a.id === _viewingAssetStoreState.id);
if (assetIndex + 1 < currentBucket.assets.length) {
return currentBucket.assets[assetIndex + 1];
}
// Get the next or previous asset
const nextIndex = direction === 'previous' ? currentIndex + 1 : currentIndex - 1;
const nextBucketIndex = currentBucketIndex + 1;
if (nextBucketIndex >= _assetGridState.buckets.length) {
return null;
}
// Run out of asset, this might be because there is no asset in the next bucket.
if (nextIndex == -1) {
let nextBucket = '';
// Find next bucket that doesn't have all assets loaded
const nextBucket = _assetGridState.buckets[nextBucketIndex];
await assetStore.getAssetsByBucket(nextBucket.bucketDate, BucketPosition.Unknown);
for (const bucket of _assetGridState.buckets) {
if (bucket.assets.length === 0) {
nextBucket = bucket.bucketDate;
break;
}
}
return nextBucket.assets[0] ?? null;
};
if (nextBucket !== '') {
await assetStore.getAssetsByBucket(nextBucket, BucketPosition.Below);
navigateAsset(direction);
}
const getPrevAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
if (assetIndex > 0) {
return currentBucket.assets[assetIndex - 1];
}
const prevBucketIndex = currentBucketIndex - 1;
if (prevBucketIndex < 0) {
return null;
}
const prevBucket = _assetGridState.buckets[prevBucketIndex];
await assetStore.getAssetsByBucket(prevBucket.bucketDate, BucketPosition.Unknown);
return prevBucket.assets[prevBucket.assets.length - 1] ?? null;
};
const navigateAsset = async (direction: 'next' | 'previous') => {
const currentAssetId = _viewingAssetStoreState.id;
const currentBucketIndex = _assetGridState.loadedAssets[currentAssetId];
if (currentBucketIndex < 0 || currentBucketIndex >= _assetGridState.buckets.length) {
return;
}
const nextAsset = assetSortedByDate[nextIndex];
if (nextAsset) {
setViewingAsset(nextAsset);
const asset =
direction === 'next'
? await getNextAsset(currentBucketIndex, currentAssetId)
: await getPrevAsset(currentBucketIndex, currentAssetId);
if (asset) {
setViewingAsset(asset);
}
};
@@ -129,14 +155,26 @@ function createAssetInteractionStore() {
selectedGroup.set(_selectedGroup);
};
const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
_assetSelectionCandidates = new Set(assets);
assetSelectionCandidates.set(_assetSelectionCandidates);
};
const clearAssetSelectionCandidates = () => {
_assetSelectionCandidates.clear();
assetSelectionCandidates.set(_assetSelectionCandidates);
};
const clearMultiselect = () => {
_selectedAssets.clear();
_selectedGroup.clear();
_assetSelectionCandidates.clear();
_assetsInAlbums = [];
selectedAssets.set(_selectedAssets);
selectedGroup.set(_selectedGroup);
assetsInAlbumStoreState.set(_assetsInAlbums);
assetSelectionCandidates.set(_assetSelectionCandidates);
};
return {
@@ -148,6 +186,8 @@ function createAssetInteractionStore() {
removeAssetFromMultiselectGroup,
addGroupToMultiselectGroup,
removeGroupFromMultiselectGroup,
setAssetSelectionCandidates,
clearAssetSelectionCandidates,
clearMultiselect,
};
}

View File

@@ -1,6 +1,5 @@
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
import { api, AssetCountByTimeBucketResponseDto } from '@api';
import { flatMap, sumBy } from 'lodash-es';
import { writable } from 'svelte/store';
/**
@@ -31,6 +30,15 @@ function createAssetStore() {
return height;
};
const refreshLoadedAssets = (state: AssetGridState): void => {
state.loadedAssets = {};
state.buckets.forEach((bucket, bucketIndex) =>
bucket.assets.map((asset) => {
state.loadedAssets[asset.id] = bucketIndex;
}),
);
};
/**
* Set initial state
* @param viewportHeight
@@ -55,12 +63,13 @@ function createAssetStore() {
position: BucketPosition.Unknown,
})),
assets: [],
loadedAssets: {},
userId,
});
// Update timeline height based on calculated bucket height
assetGridState.update((state) => {
state.timelineHeight = sumBy(state.buckets, (d) => d.bucketHeight);
state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0);
return state;
});
};
@@ -101,7 +110,8 @@ function createAssetStore() {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
state.buckets[bucketIndex].assets = assets;
state.buckets[bucketIndex].position = position;
state.assets = flatMap(state.buckets, (b) => b.assets);
state.assets = state.buckets.flatMap((b) => b.assets);
refreshLoadedAssets(state);
return state;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -123,7 +133,8 @@ function createAssetStore() {
if (state.buckets[bucketIndex].assets.length === 0) {
_removeBucket(state.buckets[bucketIndex].bucketDate);
}
state.assets = flatMap(state.buckets, (b) => b.assets);
state.assets = state.buckets.flatMap((b) => b.assets);
refreshLoadedAssets(state);
return state;
});
};
@@ -132,7 +143,8 @@ function createAssetStore() {
assetGridState.update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
state.buckets.splice(bucketIndex, 1);
state.assets = flatMap(state.buckets, (b) => b.assets);
state.assets = state.buckets.flatMap((b) => b.assets);
refreshLoadedAssets(state);
return state;
});
};
@@ -180,7 +192,8 @@ function createAssetStore() {
const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId);
state.buckets[bucketIndex].assets[assetIndex].isFavorite = isFavorite;
state.assets = flatMap(state.buckets, (b) => b.assets);
state.assets = state.buckets.flatMap((b) => b.assets);
refreshLoadedAssets(state);
return state;
});
};

View File

@@ -0,0 +1,19 @@
/**
* Calculate thumbnail size based on number of assets and viewport width
* @param assetCount Number of assets in the view
* @param viewWidth viewport width
* @returns thumbnail size
*/
export function getThumbnailSize(assetCount: number, viewWidth: number): number {
if (assetCount < 6) {
return Math.min(320, Math.floor(viewWidth / assetCount - assetCount));
} else {
if (viewWidth > 600) return viewWidth / 7 - 7;
else if (viewWidth > 400) return viewWidth / 4 - 6;
else if (viewWidth > 300) return viewWidth / 2 - 6;
else if (viewWidth > 200) return viewWidth / 2 - 6;
else if (viewWidth > 100) return viewWidth / 1 - 6;
}
return 300;
}

View File

@@ -22,10 +22,17 @@
import type { PageData } from './$types';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import FaceThumbnailSelector from '$lib/components/faces-page/face-thumbnail-selector.svelte';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
export let data: PageData;
let isEditName = false;
let isEditingName = false;
let isSelectingFace = false;
let previousRoute: string = AppRoute.EXPLORE;
let selectedAssets: Set<AssetResponseDto> = new Set();
$: isMultiSelectionMode = selectedAssets.size > 0;
@@ -41,7 +48,7 @@
const handleNameChange = async (name: string) => {
try {
isEditName = false;
isEditingName = false;
data.person.name = name;
await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { name } });
} catch (error) {
@@ -55,6 +62,25 @@
const handleSelectAll = () => {
selectedAssets = new Set(data.assets);
};
const handleSelectFeaturePhoto = async (event: CustomEvent) => {
isSelectingFace = false;
const { selectedAsset }: { selectedAsset: AssetResponseDto | undefined } = event.detail;
if (selectedAsset) {
await api.personApi.updatePerson({
id: data.person.id,
personUpdateDto: { featureFaceAssetId: selectedAsset.id },
});
// TODO: Replace by Websocket in the future
notificationController.show({
message: 'Feature photo updated, refresh page to see changes',
type: NotificationType.Info,
});
}
};
</script>
{#if isMultiSelectionMode}
@@ -73,30 +99,39 @@
</AssetSelectContextMenu>
</AssetSelectControlBar>
{:else}
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)} />
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)}>
<svelte:fragment slot="trailing">
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
<MenuOption text="Change feature photo" on:click={() => (isSelectingFace = true)} />
</AssetSelectContextMenu>
</svelte:fragment>
</ControlAppBar>
{/if}
<!-- Face information block -->
<section class="pt-24 px-4 sm:px-6 flex place-items-center">
{#if isEditName}
{#if isEditingName}
<EditNameInput
person={data.person}
on:change={(event) => handleNameChange(event.detail)}
on:cancel={() => (isEditName = false)}
on:cancel={() => (isEditingName = false)}
/>
{:else}
<ImageThumbnail
circle
shadow
url={api.getPeopleThumbnailUrl(data.person.id)}
altText={data.person.name}
widthStyle="3.375rem"
heightStyle="3.375rem"
/>
<button on:click={() => (isSelectingFace = true)}>
<ImageThumbnail
circle
shadow
url={api.getPeopleThumbnailUrl(data.person.id)}
altText={data.person.name}
widthStyle="3.375rem"
heightStyle="3.375rem"
/>
</button>
<button
title="Edit name"
class="px-4 text-immich-primary dark:text-immich-dark-primary"
on:click={() => (isEditName = true)}
on:click={() => (isEditingName = true)}
>
{#if data.person.name}
<p class="font-medium py-2">{data.person.name}</p>
@@ -109,10 +144,16 @@
</section>
<!-- Gallery Block -->
<section class="relative pt-8 sm:px-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
<section class="overflow-y-auto relative immich-scrollbar">
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
<GalleryViewer assets={data.assets} viewFrom="search-page" showArchiveIcon={true} bind:selectedAssets />
{#if !isSelectingFace}
<section class="relative pt-8 sm:px-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
<section class="overflow-y-auto relative immich-scrollbar">
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
<GalleryViewer assets={data.assets} viewFrom="search-page" showArchiveIcon={true} bind:selectedAssets />
</section>
</section>
</section>
</section>
{/if}
{#if isSelectingFace}
<FaceThumbnailSelector assets={data.assets} on:go-back={handleSelectFeaturePhoto} />
{/if}