Merge branch 'main' of https://github.com/immich-app/immich into feat/combobox-positioning

This commit is contained in:
ben-basten
2024-09-18 23:14:23 -04:00
144 changed files with 4460 additions and 5130 deletions
+287 -524
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -67,7 +67,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
+1 -1
View File
@@ -74,7 +74,7 @@
if (!theme) {
theme = { value: 'light', system: true };
} else if (theme === 'dark' || theme === 'light') {
theme = { value: item, system: false };
theme = { value: theme, system: false };
localStorage.setItem(colorThemeKeyName, JSON.stringify(theme));
} else {
theme = JSON.parse(theme);
@@ -1,5 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
@@ -9,6 +13,9 @@
import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
import { delay, isFlipped } from '$lib/utils/asset-utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { fromDateTimeOriginal, fromLocalDateTime } from '$lib/utils/timeline-util';
import {
AssetMediaSize,
getAssetInfo,
@@ -18,6 +25,7 @@
type ExifResponseDto,
} from '@immich/sdk';
import {
mdiAccountOff,
mdiCalendar,
mdiCameraIris,
mdiClose,
@@ -26,24 +34,17 @@
mdiImageOutline,
mdiInformationOutline,
mdiPencil,
mdiAccountOff,
} from '@mdi/js';
import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { slide } from 'svelte/transition';
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import PersonSidePanel from '../faces-page/person-side-panel.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import AlbumListItemDetails from './album-list-item-details.svelte';
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
import { t } from 'svelte-i18n';
import { goto } from '$app/navigation';
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
export let asset: AssetResponseDto;
@@ -100,6 +101,12 @@
$: unassignedFaces = asset.unassignedFaces || [];
$: timeZone = asset.exifInfo?.timeZone;
$: dateTime =
timeZone && asset.exifInfo?.dateTimeOriginal
? fromDateTimeOriginal(asset.exifInfo.dateTimeOriginal, timeZone)
: fromLocalDateTime(asset.localDateTime);
const dispatch = createEventDispatcher<{
close: void;
}>();
@@ -262,10 +269,7 @@
<p class="text-sm">{$t('no_exif_info_available').toUpperCase()}</p>
{/if}
{#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
{#if dateTime}
<button
type="button"
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
@@ -281,7 +285,7 @@
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{dateTime.toLocaleString(
{
month: 'short',
day: 'numeric',
@@ -292,12 +296,12 @@
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{dateTime.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
timeZoneName: timeZone ? 'longOffset' : undefined,
},
{ locale: $locale },
)}
@@ -326,17 +330,10 @@
{/if}
{#if isShowChangeDate}
{@const assetDateTimeOriginal = asset.exifInfo?.dateTimeOriginal
? DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
locale: $locale,
})
: DateTime.now()}
{@const assetTimeZoneOriginal = asset.exifInfo?.timeZone ?? ''}
<Portal>
<ChangeDate
initialDate={assetDateTimeOriginal}
initialTimeZone={assetTimeZoneOriginal}
initialDate={dateTime}
initialTimeZone={timeZone ?? ''}
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
on:cancel={() => (isShowChangeDate = false)}
/>
@@ -8,7 +8,7 @@
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
import { getAssetThumbnailUrl, getKey, handlePromiseError } from '$lib/utils';
import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk';
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text?url';
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl';
@@ -56,13 +56,14 @@
return;
}
const file = new File([blob], 'profile-picture.png', { type: 'image/png' });
const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } });
const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } });
notificationController.show({
type: NotificationType.Info,
message: $t('profile_picture_set'),
timeout: 3000,
});
$user.profileImagePath = profileImagePath;
$user.profileChangedAt = profileChangedAt;
} catch (error) {
handleError(error, $t('errors.unable_to_set_profile_picture'));
}
@@ -13,6 +13,7 @@
email: string;
profileImagePath: string;
avatarColor: UserAvatarColor;
profileChangedAt: string;
}
export let user: User;
@@ -79,7 +80,7 @@
{#if showProfileImage && user.profileImagePath}
<img
bind:this={img}
src={getProfileImageUrl(user.id)}
src={getProfileImageUrl(user)}
alt={$t('profile_image_of_user', { values: { user: title } })}
class="h-full w-full object-cover"
class:hidden={showFallback}
+20 -18
View File
@@ -638,9 +638,7 @@ export class AssetStore {
this.options.userId ||
this.options.personId ||
this.options.albumId ||
isMismatched(this.options.isArchived, asset.isArchived) ||
isMismatched(this.options.isFavorite, asset.isFavorite) ||
isMismatched(this.options.isTrashed, asset.isTrashed)
this.isExcluded(asset)
) {
// If asset is already in the bucket we don't need to recalculate
// asset store containers
@@ -699,26 +697,22 @@ export class AssetStore {
async findAndLoadBucketAsPending(id: string) {
const bucketInfo = this.assetToBucket[id];
if (bucketInfo) {
const bucket = bucketInfo.bucket;
let bucket: AssetBucket | null = bucketInfo?.bucket ?? null;
if (!bucket) {
const asset = await getAssetInfo({ id });
if (!asset || this.isExcluded(asset)) {
return;
}
bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
}
if (bucket && bucket.assets.some((a) => a.id === id)) {
this.pendingScrollBucket = bucket;
this.pendingScrollAssetId = id;
this.emit(false);
return bucket;
}
const asset = await getAssetInfo({ id });
if (asset) {
if (this.options.isArchived !== asset.isArchived) {
return;
}
const bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
if (bucket) {
this.pendingScrollBucket = bucket;
this.pendingScrollAssetId = asset.id;
this.emit(false);
}
return bucket;
}
}
/* Must be paired with matching clearPendingScroll() call */
@@ -905,6 +899,14 @@ export class AssetStore {
}
this.store$.set(this);
}
private isExcluded(asset: AssetResponseDto) {
return (
isMismatched(this.options.isArchived ?? false, asset.isArchived) ||
isMismatched(this.options.isFavorite, asset.isFavorite) ||
isMismatched(this.options.isTrashed ?? false, asset.isTrashed)
);
}
}
export const isSelectingAllAssets = writable(false);
+3 -1
View File
@@ -19,6 +19,7 @@ import {
type AssetResponseDto,
type PersonResponseDto,
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
import { sortBy } from 'lodash-es';
@@ -204,7 +205,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s
return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum });
};
export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId));
export const getProfileImageUrl = (user: UserResponseDto) =>
createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt });
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
+3
View File
@@ -36,6 +36,9 @@ export type ScrollTargetListener = ({
export const fromLocalDateTime = (localDateTime: string) =>
DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) });
export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
export const groupDateFormat: Intl.DateTimeFormatOptions = {
weekday: 'short',
month: 'short',
@@ -33,7 +33,8 @@
handlePromiseError(goto(AppRoute.PHOTOS));
}
const assetStore = new AssetStore({ isTrashed: true });
const options = { isTrashed: true };
const assetStore = new AssetStore(options);
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@@ -47,16 +48,15 @@
}
try {
await emptyTrash();
const deletedAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = deletedAssetIds.length;
assetStore.removeAssets(deletedAssetIds);
const { count } = await emptyTrash();
notificationController.show({
message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }),
message: $t('assets_permanently_deleted_count', { values: { count } }),
type: NotificationType.Info,
});
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
await assetStore.updateOptions(options);
} catch (error) {
handleError(error, $t('errors.unable_to_empty_trash'));
}
@@ -71,16 +71,14 @@
return;
}
try {
await restoreTrash();
const restoredAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = restoredAssetIds.length;
assetStore.removeAssets(restoredAssetIds);
const { count } = await restoreTrash();
notificationController.show({
message: $t('assets_restored_count', { values: { count: numberOfAssets } }),
message: $t('assets_restored_count', { values: { count } }),
type: NotificationType.Info,
});
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
await assetStore.updateOptions(options);
} catch (error) {
handleError(error, $t('errors.unable_to_restore_trash'));
}
+2 -2
View File
@@ -18,8 +18,8 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
localDateTime: Sync.each(() => faker.date.past().toISOString()),
updatedAt: Sync.each(() => faker.date.past().toISOString()),
isFavorite: Sync.each(() => faker.datatype.boolean()),
isArchived: Sync.each(() => faker.datatype.boolean()),
isTrashed: Sync.each(() => faker.datatype.boolean()),
isArchived: false,
isTrashed: false,
duration: '0:00:00.00000',
checksum: Sync.each(() => faker.string.alphanumeric(28)),
isOffline: Sync.each(() => faker.datatype.boolean()),
@@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
name: Sync.each(() => faker.person.fullName()),
profileImagePath: '',
avatarColor: UserAvatarColor.Primary,
profileChangedAt: Sync.each(() => faker.date.recent().toISOString()),
});
export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
@@ -31,4 +32,5 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({
activationKey: 'activation-key',
activatedAt: new Date().toISOString(),
},
profileChangedAt: Sync.each(() => faker.date.recent().toISOString()),
});