mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
@@ -1,67 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
|
||||
export let shared = false;
|
||||
let showAlbumPicker = false;
|
||||
export let shared = false;
|
||||
let showAlbumPicker = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const closeMenu = getMenuContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const closeMenu = getMenuContext();
|
||||
|
||||
const handleHideAlbumPicker = () => {
|
||||
showAlbumPicker = false;
|
||||
closeMenu();
|
||||
};
|
||||
const handleHideAlbumPicker = () => {
|
||||
showAlbumPicker = false;
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const handleAddToNewAlbum = (event: CustomEvent) => {
|
||||
showAlbumPicker = false;
|
||||
const handleAddToNewAlbum = (event: CustomEvent) => {
|
||||
showAlbumPicker = false;
|
||||
|
||||
const { albumName }: { albumName: string } = event.detail;
|
||||
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
||||
api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => {
|
||||
const { id, albumName } = response.data;
|
||||
const { albumName }: { albumName: string } = event.detail;
|
||||
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
||||
api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => {
|
||||
const { id, albumName } = response.data;
|
||||
|
||||
notificationController.show({
|
||||
message: `Added ${assetIds.length} to ${albumName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({
|
||||
message: `Added ${assetIds.length} to ${albumName}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
clearSelect();
|
||||
clearSelect();
|
||||
|
||||
goto('/albums/' + id);
|
||||
});
|
||||
};
|
||||
goto('/albums/' + id);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
|
||||
showAlbumPicker = false;
|
||||
const album = event.detail.album;
|
||||
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
|
||||
showAlbumPicker = false;
|
||||
const album = event.detail.album;
|
||||
|
||||
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
||||
const assetIds = Array.from(getAssets()).map((asset) => asset.id);
|
||||
|
||||
addAssetsToAlbum(album.id, assetIds).then(clearSelect);
|
||||
};
|
||||
addAssetsToAlbum(album.id, assetIds).then(clearSelect);
|
||||
};
|
||||
</script>
|
||||
|
||||
<MenuOption
|
||||
on:click={() => (showAlbumPicker = true)}
|
||||
text={shared ? 'Add to Shared Album' : 'Add to Album'}
|
||||
/>
|
||||
<MenuOption on:click={() => (showAlbumPicker = true)} text={shared ? 'Add to Shared Album' : 'Add to Album'} />
|
||||
|
||||
{#if showAlbumPicker}
|
||||
<AlbumSelectionModal
|
||||
{shared}
|
||||
on:newAlbum={handleAddToNewAlbum}
|
||||
on:newSharedAlbum={handleAddToNewAlbum}
|
||||
on:album={handleAddToAlbum}
|
||||
on:close={handleHideAlbumPicker}
|
||||
/>
|
||||
<AlbumSelectionModal
|
||||
{shared}
|
||||
on:newAlbum={handleAddToNewAlbum}
|
||||
on:newSharedAlbum={handleAddToNewAlbum}
|
||||
on:album={handleAddToAlbum}
|
||||
on:close={handleHideAlbumPicker}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||
import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { OnAssetArchive, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||
import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { OnAssetArchive, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
|
||||
export let onAssetArchive: OnAssetArchive = (asset, isArchived) => {
|
||||
asset.isArchived = isArchived;
|
||||
};
|
||||
export let onAssetArchive: OnAssetArchive = (asset, isArchived) => {
|
||||
asset.isArchived = isArchived;
|
||||
};
|
||||
|
||||
export let menuItem = false;
|
||||
export let unarchive = false;
|
||||
export let menuItem = false;
|
||||
export let unarchive = false;
|
||||
|
||||
$: text = unarchive ? 'Unarchive' : 'Archive';
|
||||
$: logo = unarchive ? ArchiveArrowUpOutline : ArchiveArrowDownOutline;
|
||||
$: text = unarchive ? 'Unarchive' : 'Archive';
|
||||
$: logo = unarchive ? ArchiveArrowUpOutline : ArchiveArrowDownOutline;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const isArchived = !unarchive;
|
||||
let cnt = 0;
|
||||
const handleArchive = async () => {
|
||||
const isArchived = !unarchive;
|
||||
let cnt = 0;
|
||||
|
||||
for (const asset of getAssets()) {
|
||||
if (asset.isArchived !== isArchived) {
|
||||
api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isArchived } });
|
||||
for (const asset of getAssets()) {
|
||||
if (asset.isArchived !== isArchived) {
|
||||
api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isArchived } });
|
||||
|
||||
onAssetArchive(asset, isArchived);
|
||||
cnt = cnt + 1;
|
||||
}
|
||||
}
|
||||
onAssetArchive(asset, isArchived);
|
||||
cnt = cnt + 1;
|
||||
}
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({
|
||||
message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
clearSelect();
|
||||
};
|
||||
clearSelect();
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
<MenuOption {text} on:click={handleArchive} />
|
||||
<MenuOption {text} on:click={handleArchive} />
|
||||
{:else}
|
||||
<CircleIconButton title={text} {logo} on:click={handleArchive} />
|
||||
<CircleIconButton title={text} {logo} on:click={handleArchive} />
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import { SharedLinkType } from '@api';
|
||||
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import { SharedLinkType } from '@api';
|
||||
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
|
||||
let showModal = false;
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
let showModal = false;
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
</script>
|
||||
|
||||
<CircleIconButton title="Share" logo={ShareVariantOutline} on:click={() => (showModal = true)} />
|
||||
|
||||
{#if showModal}
|
||||
<CreateSharedLinkModal
|
||||
sharedAssets={Array.from(getAssets())}
|
||||
shareType={SharedLinkType.Individual}
|
||||
on:close={() => {
|
||||
showModal = false;
|
||||
clearSelect();
|
||||
}}
|
||||
/>
|
||||
<CreateSharedLinkModal
|
||||
sharedAssets={Array.from(getAssets())}
|
||||
shareType={SharedLinkType.Individual}
|
||||
on:close={() => {
|
||||
showModal = false;
|
||||
clearSelect();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,74 +1,70 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
|
||||
export let onAssetDelete: OnAssetDelete;
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
export let onAssetDelete: OnAssetDelete;
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
let isShowConfirmation = false;
|
||||
let isShowConfirmation = false;
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
let count = 0;
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
let count = 0;
|
||||
|
||||
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
||||
deleteAssetDto: {
|
||||
ids: Array.from(getAssets()).map((a) => a.id)
|
||||
}
|
||||
});
|
||||
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
||||
deleteAssetDto: {
|
||||
ids: Array.from(getAssets()).map((a) => a.id),
|
||||
},
|
||||
});
|
||||
|
||||
for (const asset of deletedAssets) {
|
||||
if (asset.status === 'SUCCESS') {
|
||||
onAssetDelete(asset.id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
for (const asset of deletedAssets) {
|
||||
if (asset.status === 'SUCCESS') {
|
||||
onAssetDelete(asset.id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: `Deleted ${count}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({
|
||||
message: `Deleted ${count}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
clearSelect();
|
||||
} catch (e) {
|
||||
handleError(e, 'Error deleting assets');
|
||||
} finally {
|
||||
isShowConfirmation = false;
|
||||
}
|
||||
};
|
||||
clearSelect();
|
||||
} catch (e) {
|
||||
handleError(e, 'Error deleting assets');
|
||||
} finally {
|
||||
isShowConfirmation = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton
|
||||
title="Delete"
|
||||
logo={DeleteOutline}
|
||||
on:click={() => (isShowConfirmation = true)}
|
||||
/>
|
||||
<CircleIconButton title="Delete" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} />
|
||||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Delete Asset{getAssets().size > 1 ? 's' : ''}"
|
||||
confirmText="Delete"
|
||||
on:confirm={handleDelete}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to delete
|
||||
{#if getAssets().size > 1}
|
||||
these <b>{getAssets().size}</b> assets? This will also remove them from their album(s).
|
||||
{:else}
|
||||
this asset? This will also remove it from its album(s).
|
||||
{/if}
|
||||
</p>
|
||||
<p><b>You cannot undo this action!</b></p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
<ConfirmDialogue
|
||||
title="Delete Asset{getAssets().size > 1 ? 's' : ''}"
|
||||
confirmText="Delete"
|
||||
on:confirm={handleDelete}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to delete
|
||||
{#if getAssets().size > 1}
|
||||
these <b>{getAssets().size}</b> assets? This will also remove them from their album(s).
|
||||
{:else}
|
||||
this asset? This will also remove it from its album(s).
|
||||
{/if}
|
||||
</p>
|
||||
<p><b>You cannot undo this action!</b></p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
|
||||
export let filename = 'immich.zip';
|
||||
export let sharedLinkKey: string | undefined = undefined;
|
||||
export let menuItem = false;
|
||||
export let filename = 'immich.zip';
|
||||
export let sharedLinkKey: string | undefined = undefined;
|
||||
export let menuItem = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleDownloadFiles = async () => {
|
||||
const assets = Array.from(getAssets());
|
||||
if (assets.length === 1) {
|
||||
await downloadFile(assets[0], sharedLinkKey);
|
||||
clearSelect();
|
||||
return;
|
||||
}
|
||||
const handleDownloadFiles = async () => {
|
||||
const assets = Array.from(getAssets());
|
||||
if (assets.length === 1) {
|
||||
await downloadFile(assets[0], sharedLinkKey);
|
||||
clearSelect();
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadArchive(
|
||||
filename,
|
||||
{ assetIds: assets.map((asset) => asset.id) },
|
||||
clearSelect,
|
||||
sharedLinkKey
|
||||
);
|
||||
};
|
||||
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
<MenuOption text="Download" on:click={handleDownloadFiles} />
|
||||
<MenuOption text="Download" on:click={handleDownloadFiles} />
|
||||
{:else}
|
||||
<CircleIconButton title="Download" logo={CloudDownloadOutline} on:click={handleDownloadFiles} />
|
||||
<CircleIconButton title="Download" logo={CloudDownloadOutline} on:click={handleDownloadFiles} />
|
||||
{/if}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
|
||||
import HeartOutline from 'svelte-material-icons/HeartOutline.svelte';
|
||||
import { OnAssetFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api } from '@api';
|
||||
import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte';
|
||||
import HeartOutline from 'svelte-material-icons/HeartOutline.svelte';
|
||||
import { OnAssetFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
|
||||
export let onAssetFavorite: OnAssetFavorite = (asset, isFavorite) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
};
|
||||
export let onAssetFavorite: OnAssetFavorite = (asset, isFavorite) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
};
|
||||
|
||||
export let menuItem = false;
|
||||
export let removeFavorite: boolean;
|
||||
export let menuItem = false;
|
||||
export let removeFavorite: boolean;
|
||||
|
||||
$: text = removeFavorite ? 'Remove from Favorites' : 'Favorite';
|
||||
$: logo = removeFavorite ? HeartMinusOutline : HeartOutline;
|
||||
$: text = removeFavorite ? 'Remove from Favorites' : 'Favorite';
|
||||
$: logo = removeFavorite ? HeartMinusOutline : HeartOutline;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleFavorite = () => {
|
||||
const isFavorite = !removeFavorite;
|
||||
const handleFavorite = () => {
|
||||
const isFavorite = !removeFavorite;
|
||||
|
||||
let cnt = 0;
|
||||
for (const asset of getAssets()) {
|
||||
if (asset.isFavorite !== isFavorite) {
|
||||
api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isFavorite } });
|
||||
onAssetFavorite(asset, isFavorite);
|
||||
cnt = cnt + 1;
|
||||
}
|
||||
}
|
||||
let cnt = 0;
|
||||
for (const asset of getAssets()) {
|
||||
if (asset.isFavorite !== isFavorite) {
|
||||
api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isFavorite } });
|
||||
onAssetFavorite(asset, isFavorite);
|
||||
cnt = cnt + 1;
|
||||
}
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({
|
||||
message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
clearSelect();
|
||||
};
|
||||
clearSelect();
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
<MenuOption {text} on:click={handleFavorite} />
|
||||
<MenuOption {text} on:click={handleFavorite} />
|
||||
{:else}
|
||||
<CircleIconButton title={text} {logo} on:click={handleFavorite} />
|
||||
<CircleIconButton title={text} {logo} on:click={handleFavorite} />
|
||||
{/if}
|
||||
|
||||
@@ -1,66 +1,62 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AlbumResponseDto, api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
let isShowConfirmation = false;
|
||||
let isShowConfirmation = false;
|
||||
|
||||
const removeFromAlbum = async () => {
|
||||
try {
|
||||
const { data } = await api.albumApi.removeAssetFromAlbum({
|
||||
id: album.id,
|
||||
removeAssetsDto: {
|
||||
assetIds: Array.from(getAssets()).map((a) => a.id)
|
||||
}
|
||||
});
|
||||
const removeFromAlbum = async () => {
|
||||
try {
|
||||
const { data } = await api.albumApi.removeAssetFromAlbum({
|
||||
id: album.id,
|
||||
removeAssetsDto: {
|
||||
assetIds: Array.from(getAssets()).map((a) => a.id),
|
||||
},
|
||||
});
|
||||
|
||||
album = data;
|
||||
clearSelect();
|
||||
} catch (e) {
|
||||
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error removing assets from album, check console for more details'
|
||||
});
|
||||
} finally {
|
||||
isShowConfirmation = false;
|
||||
}
|
||||
};
|
||||
album = data;
|
||||
clearSelect();
|
||||
} catch (e) {
|
||||
console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error removing assets from album, check console for more details',
|
||||
});
|
||||
} finally {
|
||||
isShowConfirmation = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton
|
||||
title="Remove from album"
|
||||
on:click={() => (isShowConfirmation = true)}
|
||||
logo={DeleteOutline}
|
||||
/>
|
||||
<CircleIconButton title="Remove from album" on:click={() => (isShowConfirmation = true)} logo={DeleteOutline} />
|
||||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Remove Asset{getAssets().size > 1 ? 's' : ''}"
|
||||
confirmText="Remove"
|
||||
on:confirm={removeFromAlbum}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to remove
|
||||
{#if getAssets().size > 1}
|
||||
these <b>{getAssets().size}</b> assets
|
||||
{:else}
|
||||
this asset
|
||||
{/if}
|
||||
from the album?
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
<ConfirmDialogue
|
||||
title="Remove Asset{getAssets().size > 1 ? 's' : ''}"
|
||||
confirmText="Remove"
|
||||
on:confirm={removeFromAlbum}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to remove
|
||||
{#if getAssets().size > 1}
|
||||
these <b>{getAssets().size}</b> assets
|
||||
{:else}
|
||||
this asset
|
||||
{/if}
|
||||
from the album?
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
@@ -1,65 +1,58 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { SharedLinkResponseDto, api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '../../shared-components/notification/notification';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { SharedLinkResponseDto, api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { NotificationType, notificationController } from '../../shared-components/notification/notification';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
|
||||
let removing = false;
|
||||
let removing = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
|
||||
id: sharedLink.id,
|
||||
assetIdsDto: {
|
||||
assetIds: Array.from(getAssets()).map((asset) => asset.id)
|
||||
},
|
||||
key: sharedLink.key
|
||||
});
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
|
||||
id: sharedLink.id,
|
||||
assetIdsDto: {
|
||||
assetIds: Array.from(getAssets()).map((asset) => asset.id),
|
||||
},
|
||||
key: sharedLink.key,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
if (!result.success) {
|
||||
continue;
|
||||
}
|
||||
for (const result of results) {
|
||||
if (!result.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId);
|
||||
}
|
||||
sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId);
|
||||
}
|
||||
|
||||
const count = results.filter((item) => item.success).length;
|
||||
const count = results.filter((item) => item.success).length;
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `Removed ${count} assets`
|
||||
});
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `Removed ${count} assets`,
|
||||
});
|
||||
|
||||
clearSelect();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove assets from shared link');
|
||||
}
|
||||
};
|
||||
clearSelect();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove assets from shared link');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton
|
||||
title="Remove from shared link"
|
||||
on:click={() => (removing = true)}
|
||||
logo={DeleteOutline}
|
||||
/>
|
||||
<CircleIconButton title="Remove from shared link" on:click={() => (removing = true)} logo={DeleteOutline} />
|
||||
|
||||
{#if removing}
|
||||
<ConfirmDialogue
|
||||
title="Remove Assets?"
|
||||
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
|
||||
confirmText="Remove"
|
||||
on:confirm={() => handleRemove()}
|
||||
on:cancel={() => (removing = false)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
title="Remove Assets?"
|
||||
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
|
||||
confirmText="Remove"
|
||||
on:confirm={() => handleRemove()}
|
||||
on:cancel={() => (removing = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import TimerSand from 'svelte-material-icons/TimerSand.svelte';
|
||||
import { assetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetGridState, assetStore } from '$lib/stores/assets.store';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import TimerSand from 'svelte-material-icons/TimerSand.svelte';
|
||||
import { assetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetGridState, assetStore } from '$lib/stores/assets.store';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
|
||||
|
||||
let selecting = false;
|
||||
let selecting = false;
|
||||
|
||||
const handleSelectAll = async () => {
|
||||
try {
|
||||
selecting = true;
|
||||
let _assetGridState = new AssetGridState();
|
||||
assetGridState.subscribe((state) => {
|
||||
_assetGridState = state;
|
||||
});
|
||||
const handleSelectAll = async () => {
|
||||
try {
|
||||
selecting = true;
|
||||
let _assetGridState = new AssetGridState();
|
||||
assetGridState.subscribe((state) => {
|
||||
_assetGridState = state;
|
||||
});
|
||||
|
||||
for (let i = 0; i < _assetGridState.buckets.length; i++) {
|
||||
await assetStore.getAssetsByBucket(
|
||||
_assetGridState.buckets[i].bucketDate,
|
||||
BucketPosition.Unknown
|
||||
);
|
||||
for (const asset of _assetGridState.buckets[i].assets) {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
}
|
||||
selecting = false;
|
||||
} catch (e) {
|
||||
handleError(e, 'Error selecting all assets');
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < _assetGridState.buckets.length; i++) {
|
||||
await assetStore.getAssetsByBucket(_assetGridState.buckets[i].bucketDate, BucketPosition.Unknown);
|
||||
for (const asset of _assetGridState.buckets[i].assets) {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
}
|
||||
selecting = false;
|
||||
} catch (e) {
|
||||
handleError(e, 'Error selecting all assets');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if selecting}
|
||||
<CircleIconButton title="Delete" logo={TimerSand} />
|
||||
<CircleIconButton title="Delete" logo={TimerSand} />
|
||||
{/if}
|
||||
{#if !selecting}
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
|
||||
{/if}
|
||||
|
||||
@@ -1,244 +1,236 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
assetInteractionStore,
|
||||
assetsInAlbumStoreState,
|
||||
isMultiSelectStoreState,
|
||||
selectedAssets,
|
||||
selectedGroup
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import lodash from 'lodash-es';
|
||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
assetsInAlbumStoreState,
|
||||
isMultiSelectStoreState,
|
||||
selectedAssets,
|
||||
selectedGroup,
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import lodash from 'lodash-es';
|
||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
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;
|
||||
export let bucketHeight: number;
|
||||
export let isAlbumSelectionMode = false;
|
||||
export let viewportWidth: number;
|
||||
export let assets: AssetResponseDto[];
|
||||
export let bucketDate: string;
|
||||
export let bucketHeight: number;
|
||||
export let isAlbumSelectionMode = false;
|
||||
export let viewportWidth: number;
|
||||
|
||||
const groupDateFormat: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
const groupDateFormat: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let isMouseOverGroup = false;
|
||||
let actualBucketHeight: number;
|
||||
let hoveredDateGroup = '';
|
||||
let isMouseOverGroup = false;
|
||||
let actualBucketHeight: number;
|
||||
let hoveredDateGroup = '';
|
||||
|
||||
interface LayoutBox {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
interface LayoutBox {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
$: assetsGroupByDate = lodash
|
||||
.chain(assets)
|
||||
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString($locale, groupDateFormat))
|
||||
.sortBy((group) => assets.indexOf(group[0]))
|
||||
.value();
|
||||
$: assetsGroupByDate = lodash
|
||||
.chain(assets)
|
||||
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString($locale, groupDateFormat))
|
||||
.sortBy((group) => assets.indexOf(group[0]))
|
||||
.value();
|
||||
|
||||
$: geometry = (() => {
|
||||
const geometry = [];
|
||||
for (let group of assetsGroupByDate) {
|
||||
const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), {
|
||||
boxSpacing: 2,
|
||||
containerWidth: Math.floor(viewportWidth),
|
||||
containerPadding: 0,
|
||||
targetRowHeightTolerance: 0.15,
|
||||
targetRowHeight: 235
|
||||
});
|
||||
geometry.push({
|
||||
...justifiedLayoutResult,
|
||||
containerWidth: calculateWidth(justifiedLayoutResult.boxes)
|
||||
});
|
||||
}
|
||||
return geometry;
|
||||
})();
|
||||
$: geometry = (() => {
|
||||
const geometry = [];
|
||||
for (let group of assetsGroupByDate) {
|
||||
const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), {
|
||||
boxSpacing: 2,
|
||||
containerWidth: Math.floor(viewportWidth),
|
||||
containerPadding: 0,
|
||||
targetRowHeightTolerance: 0.15,
|
||||
targetRowHeight: 235,
|
||||
});
|
||||
geometry.push({
|
||||
...justifiedLayoutResult,
|
||||
containerWidth: calculateWidth(justifiedLayoutResult.boxes),
|
||||
});
|
||||
}
|
||||
return geometry;
|
||||
})();
|
||||
|
||||
$: {
|
||||
if (actualBucketHeight && actualBucketHeight != 0 && actualBucketHeight != bucketHeight) {
|
||||
const heightDelta = assetStore.updateBucketHeight(bucketDate, actualBucketHeight);
|
||||
if (heightDelta !== 0) {
|
||||
scrollTimeline(heightDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
$: {
|
||||
if (actualBucketHeight && actualBucketHeight != 0 && actualBucketHeight != bucketHeight) {
|
||||
const heightDelta = assetStore.updateBucketHeight(bucketDate, actualBucketHeight);
|
||||
if (heightDelta !== 0) {
|
||||
scrollTimeline(heightDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollTimeline(heightDelta: number) {
|
||||
dispatch('shift', {
|
||||
heightDelta
|
||||
});
|
||||
}
|
||||
function scrollTimeline(heightDelta: number) {
|
||||
dispatch('shift', {
|
||||
heightDelta,
|
||||
});
|
||||
}
|
||||
|
||||
const calculateWidth = (boxes: LayoutBox[]): number => {
|
||||
let width = 0;
|
||||
for (const box of boxes) {
|
||||
if (box.top < 100) {
|
||||
width = box.left + box.width;
|
||||
}
|
||||
}
|
||||
const calculateWidth = (boxes: LayoutBox[]): number => {
|
||||
let width = 0;
|
||||
for (const box of boxes) {
|
||||
if (box.top < 100) {
|
||||
width = box.left + box.width;
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
};
|
||||
return width;
|
||||
};
|
||||
|
||||
const assetClickHandler = (
|
||||
asset: AssetResponseDto,
|
||||
assetsInDateGroup: AssetResponseDto[],
|
||||
dateGroupTitle: string
|
||||
) => {
|
||||
if (isAlbumSelectionMode) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||
return;
|
||||
}
|
||||
const assetClickHandler = (
|
||||
asset: AssetResponseDto,
|
||||
assetsInDateGroup: AssetResponseDto[],
|
||||
dateGroupTitle: string,
|
||||
) => {
|
||||
if (isAlbumSelectionMode) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isMultiSelectStoreState) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.setViewingAsset(asset);
|
||||
}
|
||||
};
|
||||
if ($isMultiSelectStoreState) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.setViewingAsset(asset);
|
||||
}
|
||||
};
|
||||
|
||||
const selectAssetGroupHandler = (
|
||||
selectAssetGroupHandler: AssetResponseDto[],
|
||||
dateGroupTitle: string
|
||||
) => {
|
||||
if ($selectedGroup.has(dateGroupTitle)) {
|
||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
selectAssetGroupHandler.forEach((asset) => {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
});
|
||||
} else {
|
||||
assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
selectAssetGroupHandler.forEach((asset) => {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
});
|
||||
}
|
||||
};
|
||||
const selectAssetGroupHandler = (selectAssetGroupHandler: AssetResponseDto[], dateGroupTitle: string) => {
|
||||
if ($selectedGroup.has(dateGroupTitle)) {
|
||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
selectAssetGroupHandler.forEach((asset) => {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
});
|
||||
} else {
|
||||
assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
selectAssetGroupHandler.forEach((asset) => {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const assetSelectHandler = (
|
||||
asset: AssetResponseDto,
|
||||
assetsInDateGroup: AssetResponseDto[],
|
||||
dateGroupTitle: string
|
||||
) => {
|
||||
if ($selectedAssets.has(asset)) {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
} else {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
const assetSelectHandler = (
|
||||
asset: AssetResponseDto,
|
||||
assetsInDateGroup: AssetResponseDto[],
|
||||
dateGroupTitle: string,
|
||||
) => {
|
||||
if ($selectedAssets.has(asset)) {
|
||||
assetInteractionStore.removeAssetFromMultiselectGroup(asset);
|
||||
} else {
|
||||
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++;
|
||||
}
|
||||
});
|
||||
// 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++;
|
||||
}
|
||||
});
|
||||
|
||||
// if all assets are selected in a group, add the group to selected group
|
||||
if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
|
||||
assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
}
|
||||
};
|
||||
// if all assets are selected in a group, add the group to selected group
|
||||
if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
|
||||
assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle);
|
||||
}
|
||||
};
|
||||
|
||||
const assetMouseEventHandler = (dateGroupTitle: string) => {
|
||||
// Show multi select icon on hover on date group
|
||||
hoveredDateGroup = dateGroupTitle;
|
||||
};
|
||||
const assetMouseEventHandler = (dateGroupTitle: string) => {
|
||||
// Show multi select icon on hover on date group
|
||||
hoveredDateGroup = dateGroupTitle;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="asset-group-by-date"
|
||||
class="flex flex-wrap gap-x-12"
|
||||
bind:clientHeight={actualBucketHeight}
|
||||
bind:clientWidth={viewportWidth}
|
||||
id="asset-group-by-date"
|
||||
class="flex flex-wrap gap-x-12"
|
||||
bind:clientHeight={actualBucketHeight}
|
||||
bind:clientWidth={viewportWidth}
|
||||
>
|
||||
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
||||
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
|
||||
$locale,
|
||||
groupDateFormat
|
||||
)}
|
||||
<!-- Asset Group By Date -->
|
||||
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
||||
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString($locale, groupDateFormat)}
|
||||
<!-- Asset Group By Date -->
|
||||
|
||||
<div
|
||||
class="flex flex-col mt-5"
|
||||
on:mouseenter={() => {
|
||||
isMouseOverGroup = true;
|
||||
assetMouseEventHandler(dateGroupTitle);
|
||||
}}
|
||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<p
|
||||
class="font-medium text-xs md:text-sm text-immich-fg dark:text-immich-dark-fg mb-2 flex place-items-center h-6"
|
||||
style="width: {geometry[groupIndex].containerWidth}px"
|
||||
>
|
||||
{#if (hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle)}
|
||||
<div
|
||||
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||
class="inline-block px-2 hover:cursor-pointer"
|
||||
on:click={() => selectAssetGroupHandler(assetsInDateGroup, dateGroupTitle)}
|
||||
on:keydown={() => selectAssetGroupHandler(assetsInDateGroup, dateGroupTitle)}
|
||||
>
|
||||
{#if $selectedGroup.has(dateGroupTitle)}
|
||||
<CheckCircle size="24" color="#4250af" />
|
||||
{:else}
|
||||
<CircleOutline size="24" color="#757575" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="flex flex-col mt-5"
|
||||
on:mouseenter={() => {
|
||||
isMouseOverGroup = true;
|
||||
assetMouseEventHandler(dateGroupTitle);
|
||||
}}
|
||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<p
|
||||
class="font-medium text-xs md:text-sm text-immich-fg dark:text-immich-dark-fg mb-2 flex place-items-center h-6"
|
||||
style="width: {geometry[groupIndex].containerWidth}px"
|
||||
>
|
||||
{#if (hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle)}
|
||||
<div
|
||||
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||
class="inline-block px-2 hover:cursor-pointer"
|
||||
on:click={() => selectAssetGroupHandler(assetsInDateGroup, dateGroupTitle)}
|
||||
on:keydown={() => selectAssetGroupHandler(assetsInDateGroup, dateGroupTitle)}
|
||||
>
|
||||
{#if $selectedGroup.has(dateGroupTitle)}
|
||||
<CheckCircle size="24" color="#4250af" />
|
||||
{:else}
|
||||
<CircleOutline size="24" color="#757575" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="truncate" title={dateGroupTitle}>
|
||||
{dateGroupTitle}
|
||||
</span>
|
||||
</p>
|
||||
<span class="truncate" title={dateGroupTitle}>
|
||||
{dateGroupTitle}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Image grid -->
|
||||
<div
|
||||
class="relative"
|
||||
style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex]
|
||||
.containerWidth}px"
|
||||
>
|
||||
{#each assetsInDateGroup as asset, index (asset.id)}
|
||||
{@const box = geometry[groupIndex].boxes[index]}
|
||||
<div
|
||||
class="absolute"
|
||||
style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px"
|
||||
>
|
||||
<Thumbnail
|
||||
{asset}
|
||||
{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}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<!-- Image grid -->
|
||||
<div
|
||||
class="relative"
|
||||
style="height: {geometry[groupIndex].containerHeight}px;width: {geometry[groupIndex].containerWidth}px"
|
||||
>
|
||||
{#each assetsInDateGroup as asset, index (asset.id)}
|
||||
{@const box = geometry[groupIndex].boxes[index]}
|
||||
<div
|
||||
class="absolute"
|
||||
style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px"
|
||||
>
|
||||
<Thumbnail
|
||||
{asset}
|
||||
{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}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#asset-group-by-date {
|
||||
contain: layout;
|
||||
}
|
||||
#asset-group-by-date {
|
||||
contain: layout;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,190 +1,190 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isViewingAssetStoreState,
|
||||
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 { onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
|
||||
import Portal from '../shared-components/portal/portal.svelte';
|
||||
import Scrollbar, {
|
||||
OnScrollbarClickDetail,
|
||||
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';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
isViewingAssetStoreState,
|
||||
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 { onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
|
||||
import Portal from '../shared-components/portal/portal.svelte';
|
||||
import Scrollbar, {
|
||||
OnScrollbarClickDetail,
|
||||
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;
|
||||
export let isAlbumSelectionMode = false;
|
||||
export let showMemoryLane = false;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
export let isAlbumSelectionMode = false;
|
||||
export let showMemoryLane = false;
|
||||
|
||||
let viewportHeight = 0;
|
||||
let viewportWidth = 0;
|
||||
let assetGridElement: HTMLElement;
|
||||
let bucketInfo: AssetCountByTimeBucketResponseDto;
|
||||
let viewportHeight = 0;
|
||||
let viewportWidth = 0;
|
||||
let assetGridElement: HTMLElement;
|
||||
let bucketInfo: AssetCountByTimeBucketResponseDto;
|
||||
|
||||
onMount(async () => {
|
||||
const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({
|
||||
getAssetCountByTimeBucketDto: {
|
||||
timeGroup: TimeGroupEnum.Month,
|
||||
userId: user?.id,
|
||||
withoutThumbs: true
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({
|
||||
getAssetCountByTimeBucketDto: {
|
||||
timeGroup: TimeGroupEnum.Month,
|
||||
userId: user?.id,
|
||||
withoutThumbs: true,
|
||||
},
|
||||
});
|
||||
|
||||
bucketInfo = assetCountByTimebucket;
|
||||
bucketInfo = assetCountByTimebucket;
|
||||
|
||||
assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket, user?.id);
|
||||
assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket, user?.id);
|
||||
|
||||
// Get asset bucket if bucket height is smaller than viewport height
|
||||
let bucketsToFetchInitially: string[] = [];
|
||||
let initialBucketsHeight = 0;
|
||||
$assetGridState.buckets.every((bucket) => {
|
||||
if (initialBucketsHeight < viewportHeight) {
|
||||
initialBucketsHeight += bucket.bucketHeight;
|
||||
bucketsToFetchInitially.push(bucket.bucketDate);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// Get asset bucket if bucket height is smaller than viewport height
|
||||
let bucketsToFetchInitially: string[] = [];
|
||||
let initialBucketsHeight = 0;
|
||||
$assetGridState.buckets.every((bucket) => {
|
||||
if (initialBucketsHeight < viewportHeight) {
|
||||
initialBucketsHeight += bucket.bucketHeight;
|
||||
bucketsToFetchInitially.push(bucket.bucketDate);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
bucketsToFetchInitially.forEach((bucketDate) => {
|
||||
assetStore.getAssetsByBucket(bucketDate, BucketPosition.Visible);
|
||||
});
|
||||
});
|
||||
bucketsToFetchInitially.forEach((bucketDate) => {
|
||||
assetStore.getAssetsByBucket(bucketDate, BucketPosition.Visible);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.setInitialState(0, 0, { totalCount: 0, buckets: [] }, undefined);
|
||||
});
|
||||
onDestroy(() => {
|
||||
assetStore.setInitialState(0, 0, { totalCount: 0, buckets: [] }, undefined);
|
||||
});
|
||||
|
||||
function intersectedHandler(event: CustomEvent) {
|
||||
const el = event.detail.container as HTMLElement;
|
||||
const target = el.firstChild as HTMLElement;
|
||||
if (target) {
|
||||
const bucketDate = target.id.split('_')[1];
|
||||
assetStore.getAssetsByBucket(bucketDate, event.detail.position);
|
||||
}
|
||||
}
|
||||
function intersectedHandler(event: CustomEvent) {
|
||||
const el = event.detail.container as HTMLElement;
|
||||
const target = el.firstChild as HTMLElement;
|
||||
if (target) {
|
||||
const bucketDate = target.id.split('_')[1];
|
||||
assetStore.getAssetsByBucket(bucketDate, event.detail.position);
|
||||
}
|
||||
}
|
||||
|
||||
function handleScrollTimeline(event: CustomEvent) {
|
||||
assetGridElement.scrollBy(0, event.detail.heightDelta);
|
||||
}
|
||||
function handleScrollTimeline(event: CustomEvent) {
|
||||
assetGridElement.scrollBy(0, event.detail.heightDelta);
|
||||
}
|
||||
|
||||
const navigateToPreviousAsset = () => {
|
||||
assetInteractionStore.navigateAsset('previous');
|
||||
};
|
||||
const navigateToPreviousAsset = () => {
|
||||
assetInteractionStore.navigateAsset('previous');
|
||||
};
|
||||
|
||||
const navigateToNextAsset = () => {
|
||||
assetInteractionStore.navigateAsset('next');
|
||||
};
|
||||
const navigateToNextAsset = () => {
|
||||
assetInteractionStore.navigateAsset('next');
|
||||
};
|
||||
|
||||
let lastScrollPosition = 0;
|
||||
let animationTick = false;
|
||||
let lastScrollPosition = 0;
|
||||
let animationTick = false;
|
||||
|
||||
const handleTimelineScroll = () => {
|
||||
if (!animationTick) {
|
||||
window.requestAnimationFrame(() => {
|
||||
lastScrollPosition = assetGridElement?.scrollTop;
|
||||
animationTick = false;
|
||||
});
|
||||
const handleTimelineScroll = () => {
|
||||
if (!animationTick) {
|
||||
window.requestAnimationFrame(() => {
|
||||
lastScrollPosition = assetGridElement?.scrollTop;
|
||||
animationTick = false;
|
||||
});
|
||||
|
||||
animationTick = true;
|
||||
}
|
||||
};
|
||||
animationTick = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollbarClick = (e: OnScrollbarClickDetail) => {
|
||||
assetGridElement.scrollTop = e.scrollTo;
|
||||
};
|
||||
const handleScrollbarClick = (e: OnScrollbarClickDetail) => {
|
||||
assetGridElement.scrollTop = e.scrollTo;
|
||||
};
|
||||
|
||||
const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
|
||||
assetGridElement.scrollTop = e.scrollTo;
|
||||
};
|
||||
const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
|
||||
assetGridElement.scrollTop = e.scrollTo;
|
||||
};
|
||||
|
||||
const handleArchiveSuccess = (e: CustomEvent) => {
|
||||
const asset = e.detail as AssetResponseDto;
|
||||
navigateToNextAsset();
|
||||
assetStore.removeAsset(asset.id);
|
||||
};
|
||||
const handleArchiveSuccess = (e: CustomEvent) => {
|
||||
const asset = e.detail as AssetResponseDto;
|
||||
navigateToNextAsset();
|
||||
assetStore.removeAsset(asset.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
|
||||
<Scrollbar
|
||||
scrollbarHeight={viewportHeight}
|
||||
scrollTop={lastScrollPosition}
|
||||
on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
|
||||
on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
|
||||
/>
|
||||
<Scrollbar
|
||||
scrollbarHeight={viewportHeight}
|
||||
scrollTop={lastScrollPosition}
|
||||
on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
|
||||
on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
|
||||
<section
|
||||
id="asset-grid"
|
||||
class="overflow-y-auto ml-4 mb-4 mr-[60px] scrollbar-hidden"
|
||||
bind:clientHeight={viewportHeight}
|
||||
bind:clientWidth={viewportWidth}
|
||||
bind:this={assetGridElement}
|
||||
on:scroll={handleTimelineScroll}
|
||||
id="asset-grid"
|
||||
class="overflow-y-auto ml-4 mb-4 mr-[60px] scrollbar-hidden"
|
||||
bind:clientHeight={viewportHeight}
|
||||
bind:clientWidth={viewportWidth}
|
||||
bind:this={assetGridElement}
|
||||
on:scroll={handleTimelineScroll}
|
||||
>
|
||||
{#if assetGridElement}
|
||||
{#if showMemoryLane}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
<section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
|
||||
{#each $assetGridState.buckets as bucket, bucketIndex (bucketIndex)}
|
||||
<IntersectionObserver
|
||||
on:intersected={intersectedHandler}
|
||||
on:hidden={async () => {
|
||||
// If bucket is hidden and in loading state, cancel the request
|
||||
if ($loadingBucketState[bucket.bucketDate]) {
|
||||
await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate);
|
||||
}
|
||||
}}
|
||||
let:intersecting
|
||||
top={750}
|
||||
bottom={750}
|
||||
root={assetGridElement}
|
||||
>
|
||||
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
|
||||
{#if intersecting}
|
||||
<AssetDateGroup
|
||||
{isAlbumSelectionMode}
|
||||
on:shift={handleScrollTimeline}
|
||||
assets={bucket.assets}
|
||||
bucketDate={bucket.bucketDate}
|
||||
bucketHeight={bucket.bucketHeight}
|
||||
{viewportWidth}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</IntersectionObserver>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if assetGridElement}
|
||||
{#if showMemoryLane}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
<section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
|
||||
{#each $assetGridState.buckets as bucket, bucketIndex (bucketIndex)}
|
||||
<IntersectionObserver
|
||||
on:intersected={intersectedHandler}
|
||||
on:hidden={async () => {
|
||||
// If bucket is hidden and in loading state, cancel the request
|
||||
if ($loadingBucketState[bucket.bucketDate]) {
|
||||
await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate);
|
||||
}
|
||||
}}
|
||||
let:intersecting
|
||||
top={750}
|
||||
bottom={750}
|
||||
root={assetGridElement}
|
||||
>
|
||||
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
|
||||
{#if intersecting}
|
||||
<AssetDateGroup
|
||||
{isAlbumSelectionMode}
|
||||
on:shift={handleScrollTimeline}
|
||||
assets={bucket.assets}
|
||||
bucketDate={bucket.bucketDate}
|
||||
bucketHeight={bucket.bucketHeight}
|
||||
{viewportWidth}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</IntersectionObserver>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<Portal target="body">
|
||||
{#if $isViewingAssetStoreState}
|
||||
<AssetViewer
|
||||
asset={$viewingAssetStoreState}
|
||||
on:navigate-previous={navigateToPreviousAsset}
|
||||
on:navigate-next={navigateToNextAsset}
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
}}
|
||||
on:archived={handleArchiveSuccess}
|
||||
/>
|
||||
{/if}
|
||||
{#if $isViewingAssetStoreState}
|
||||
<AssetViewer
|
||||
asset={$viewingAssetStoreState}
|
||||
on:navigate-previous={navigateToPreviousAsset}
|
||||
on:navigate-next={navigateToNextAsset}
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
}}
|
||||
on:archived={handleArchiveSuccess}
|
||||
/>
|
||||
{/if}
|
||||
</Portal>
|
||||
|
||||
<style>
|
||||
#asset-grid {
|
||||
contain: layout;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
#asset-grid {
|
||||
contain: layout;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
import { createContext } from '$lib/utils/context';
|
||||
import { createContext } from '$lib/utils/context';
|
||||
|
||||
const { get: getMenuContext, set: setContext } = createContext<() => void>();
|
||||
export { getMenuContext };
|
||||
const { get: getMenuContext, set: setContext } = createContext<() => void>();
|
||||
export { getMenuContext };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import type Icon from 'svelte-material-icons/AbTesting.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import type Icon from 'svelte-material-icons/AbTesting.svelte';
|
||||
|
||||
export let icon: typeof Icon;
|
||||
export let title: string;
|
||||
export let icon: typeof Icon;
|
||||
export let title: string;
|
||||
|
||||
let showContextMenu = false;
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
let showContextMenu = false;
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
|
||||
const handleShowMenu = ({ x, y }: MouseEvent) => {
|
||||
contextMenuPosition = { x, y };
|
||||
showContextMenu = !showContextMenu;
|
||||
};
|
||||
const handleShowMenu = ({ x, y }: MouseEvent) => {
|
||||
contextMenuPosition = { x, y };
|
||||
showContextMenu = !showContextMenu;
|
||||
};
|
||||
|
||||
setContext(() => (showContextMenu = false));
|
||||
setContext(() => (showContextMenu = false));
|
||||
</script>
|
||||
|
||||
<CircleIconButton {title} logo={icon} on:click={handleShowMenu} />
|
||||
|
||||
{#if showContextMenu}
|
||||
<ContextMenu {...contextMenuPosition} on:outclick={() => (showContextMenu = false)}>
|
||||
<div class="flex flex-col rounded-lg">
|
||||
<slot />
|
||||
</div>
|
||||
</ContextMenu>
|
||||
<ContextMenu {...contextMenuPosition} on:outclick={() => (showContextMenu = false)}>
|
||||
<div class="flex flex-col rounded-lg">
|
||||
<slot />
|
||||
</div>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
import { createContext } from '$lib/utils/context';
|
||||
import { createContext } from '$lib/utils/context';
|
||||
|
||||
export type OnAssetDelete = (assetId: string) => void;
|
||||
export type OnAssetArchive = (asset: AssetResponseDto, archived: boolean) => void;
|
||||
export type OnAssetFavorite = (asset: AssetResponseDto, favorite: boolean) => void;
|
||||
export type OnAssetDelete = (assetId: string) => void;
|
||||
export type OnAssetArchive = (asset: AssetResponseDto, archived: boolean) => void;
|
||||
export type OnAssetFavorite = (asset: AssetResponseDto, favorite: boolean) => void;
|
||||
|
||||
export interface AssetControlContext {
|
||||
// Wrap assets in a function, because context isn't reactive.
|
||||
getAssets: () => Set<AssetResponseDto>;
|
||||
clearSelect: () => void;
|
||||
}
|
||||
export interface AssetControlContext {
|
||||
// Wrap assets in a function, because context isn't reactive.
|
||||
getAssets: () => Set<AssetResponseDto>;
|
||||
clearSelect: () => void;
|
||||
}
|
||||
|
||||
const { get: getAssetControlContext, set: setContext } = createContext<AssetControlContext>();
|
||||
export { getAssetControlContext };
|
||||
const { get: getAssetControlContext, set: setContext } = createContext<AssetControlContext>();
|
||||
export { getAssetControlContext };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
|
||||
export let assets: Set<AssetResponseDto>;
|
||||
export let clearSelect: () => void;
|
||||
export let assets: Set<AssetResponseDto>;
|
||||
export let clearSelect: () => void;
|
||||
|
||||
setContext({ getAssets: () => assets, clearSelect });
|
||||
setContext({ getAssets: () => assets, clearSelect });
|
||||
</script>
|
||||
|
||||
<ControlAppBar
|
||||
on:close-button-click={clearSelect}
|
||||
backIcon={Close}
|
||||
tailwindClasses="bg-white shadow-md"
|
||||
>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
||||
Selected {assets.size.toLocaleString($locale)}
|
||||
</p>
|
||||
<slot slot="trailing" />
|
||||
<ControlAppBar on:close-button-click={clearSelect} backIcon={Close} tailwindClasses="bg-white shadow-md">
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
||||
Selected {assets.size.toLocaleString($locale)}
|
||||
</p>
|
||||
<slot slot="trailing" />
|
||||
</ControlAppBar>
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { api } from '@api';
|
||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||
import { memoryStore } from '$lib/stores/memory.store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onMount } from 'svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { api } from '@api';
|
||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||
import { memoryStore } from '$lib/stores/memory.store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
$: shouldRender = $memoryStore?.length > 0;
|
||||
$: shouldRender = $memoryStore?.length > 0;
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.assetApi.getMemoryLane({
|
||||
timestamp: DateTime.local().startOf('day').toISO() || ''
|
||||
});
|
||||
$memoryStore = data;
|
||||
});
|
||||
onMount(async () => {
|
||||
const { data } = await api.assetApi.getMemoryLane({
|
||||
timestamp: DateTime.local().startOf('day').toISO() || '',
|
||||
});
|
||||
$memoryStore = data;
|
||||
});
|
||||
|
||||
let memoryLaneElement: HTMLElement;
|
||||
let offsetWidth = 0;
|
||||
let innerWidth = 0;
|
||||
let memoryLaneElement: HTMLElement;
|
||||
let offsetWidth = 0;
|
||||
let innerWidth = 0;
|
||||
|
||||
let scrollLeftPosition = 0;
|
||||
let scrollLeftPosition = 0;
|
||||
|
||||
const onScroll = () => (scrollLeftPosition = memoryLaneElement?.scrollLeft);
|
||||
const onScroll = () => (scrollLeftPosition = memoryLaneElement?.scrollLeft);
|
||||
|
||||
$: canScrollLeft = scrollLeftPosition > 0;
|
||||
$: canScrollRight = Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth;
|
||||
$: canScrollLeft = scrollLeftPosition > 0;
|
||||
$: canScrollRight = Math.ceil(scrollLeftPosition) < innerWidth - offsetWidth;
|
||||
|
||||
const scrollBy = 400;
|
||||
const scrollLeft = () => memoryLaneElement.scrollBy({ left: -scrollBy, behavior: 'smooth' });
|
||||
const scrollRight = () => memoryLaneElement.scrollBy({ left: scrollBy, behavior: 'smooth' });
|
||||
const scrollBy = 400;
|
||||
const scrollLeft = () => memoryLaneElement.scrollBy({ left: -scrollBy, behavior: 'smooth' });
|
||||
const scrollRight = () => memoryLaneElement.scrollBy({ left: scrollBy, behavior: 'smooth' });
|
||||
</script>
|
||||
|
||||
{#if shouldRender}
|
||||
<section
|
||||
id="memory-lane"
|
||||
bind:this={memoryLaneElement}
|
||||
class="relative overflow-x-hidden whitespace-nowrap mt-5 transition-all"
|
||||
bind:offsetWidth
|
||||
on:scroll={onScroll}
|
||||
>
|
||||
{#if canScrollLeft || canScrollRight}
|
||||
<div class="sticky left-0 z-20">
|
||||
{#if canScrollLeft}
|
||||
<div class="absolute left-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
class="rounded-full opacity-50 hover:opacity-100 p-2 border border-gray-500 bg-gray-100 text-gray-500"
|
||||
on:click={scrollLeft}
|
||||
>
|
||||
<ChevronLeft size="36" /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if canScrollRight}
|
||||
<div class="absolute right-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
class="rounded-full opacity-50 hover:opacity-100 p-2 border border-gray-500 bg-gray-100 text-gray-500"
|
||||
on:click={scrollRight}
|
||||
>
|
||||
<ChevronRight size="36" /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<section
|
||||
id="memory-lane"
|
||||
bind:this={memoryLaneElement}
|
||||
class="relative overflow-x-hidden whitespace-nowrap mt-5 transition-all"
|
||||
bind:offsetWidth
|
||||
on:scroll={onScroll}
|
||||
>
|
||||
{#if canScrollLeft || canScrollRight}
|
||||
<div class="sticky left-0 z-20">
|
||||
{#if canScrollLeft}
|
||||
<div class="absolute left-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
class="rounded-full opacity-50 hover:opacity-100 p-2 border border-gray-500 bg-gray-100 text-gray-500"
|
||||
on:click={scrollLeft}
|
||||
>
|
||||
<ChevronLeft size="36" /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if canScrollRight}
|
||||
<div class="absolute right-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
class="rounded-full opacity-50 hover:opacity-100 p-2 border border-gray-500 bg-gray-100 text-gray-500"
|
||||
on:click={scrollRight}
|
||||
>
|
||||
<ChevronRight size="36" /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
||||
{#each $memoryStore as memory, i (memory.title)}
|
||||
<button
|
||||
class="memory-card relative inline-block mr-8 rounded-xl aspect-video h-[215px]"
|
||||
on:click={() => goto(`/memory?memory=${i}`)}
|
||||
>
|
||||
<img
|
||||
class="rounded-xl h-full w-full object-cover"
|
||||
src={api.getAssetThumbnailUrl(memory.assets[0].id, 'JPEG')}
|
||||
alt={memory.title}
|
||||
draggable="false"
|
||||
/>
|
||||
<p class="absolute bottom-2 left-4 text-lg text-white z-10">{memory.title}</p>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent z-0 hover:bg-black/20 transition-all"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
||||
{#each $memoryStore as memory, i (memory.title)}
|
||||
<button
|
||||
class="memory-card relative inline-block mr-8 rounded-xl aspect-video h-[215px]"
|
||||
on:click={() => goto(`/memory?memory=${i}`)}
|
||||
>
|
||||
<img
|
||||
class="rounded-xl h-full w-full object-cover"
|
||||
src={api.getAssetThumbnailUrl(memory.assets[0].id, 'JPEG')}
|
||||
alt={memory.title}
|
||||
draggable="false"
|
||||
/>
|
||||
<p class="absolute bottom-2 left-4 text-lg text-white z-10">{memory.title}</p>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent z-0 hover:bg-black/20 transition-all"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.memory-card {
|
||||
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
|
||||
}
|
||||
.memory-card {
|
||||
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user