Merge branch 'main' of https://github.com/immich-app/immich into feat/combobox-positioning
This commit is contained in:
Generated
+287
-524
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user