forked from Cutlery/immich
* Added context menu for album opionts * choose asset for album thumbnail * Refactor UpdateAlbumDto to accept albumThumbnailAssetId * implemented changing album cover on web * Fixed api change on mobile app
240 lines
7.5 KiB
Svelte
240 lines
7.5 KiB
Svelte
<script lang="ts">
|
|
import { createEventDispatcher, onMount } from 'svelte';
|
|
import { quintOut } from 'svelte/easing';
|
|
import { fly } from 'svelte/transition';
|
|
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
|
|
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
|
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
|
import moment from 'moment';
|
|
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
|
import { AssetResponseDto } from '@api';
|
|
import AlbumAppBar from './album-app-bar.svelte';
|
|
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
|
|
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let assetsInAlbum: AssetResponseDto[];
|
|
|
|
let selectedAsset: Set<string> = new Set();
|
|
let selectedGroup: Set<number> = new Set();
|
|
let existingGroup: Set<number> = new Set();
|
|
let groupWithAssetsInAlbum: Record<number, Set<string>> = {};
|
|
|
|
let uploadAssets: string[] = [];
|
|
let uploadAssetsCount = 9999;
|
|
|
|
onMount(() => {
|
|
scanForExistingSelectedGroup();
|
|
|
|
albumUploadAssetStore.asset.subscribe((uploadedAsset) => {
|
|
uploadAssets = uploadedAsset;
|
|
});
|
|
|
|
albumUploadAssetStore.count.subscribe((count) => {
|
|
uploadAssetsCount = count;
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Watch for the uploading event - when the uploaded assets are the same number of the chosen asset
|
|
* navigate back and add them to the album
|
|
*/
|
|
$: {
|
|
if (uploadAssets.length == uploadAssetsCount) {
|
|
// Filter assets that are already in the album
|
|
const assetsToAdd = uploadAssets.filter(
|
|
(asset) => !assetsInAlbum.some((a) => a.id === asset)
|
|
);
|
|
// Add the just uploaded assets to the album
|
|
dispatch('create-album', {
|
|
assets: assetsToAdd
|
|
});
|
|
|
|
// Clean up states.
|
|
albumUploadAssetStore.asset.set([]);
|
|
albumUploadAssetStore.count.set(9999);
|
|
}
|
|
}
|
|
|
|
const selectAssetHandler = (assetId: string, groupIndex: number) => {
|
|
const tempSelectedAsset = new Set(selectedAsset);
|
|
|
|
if (selectedAsset.has(assetId)) {
|
|
tempSelectedAsset.delete(assetId);
|
|
|
|
const tempSelectedGroup = new Set(selectedGroup);
|
|
tempSelectedGroup.delete(groupIndex);
|
|
selectedGroup = tempSelectedGroup;
|
|
} else {
|
|
tempSelectedAsset.add(assetId);
|
|
}
|
|
|
|
selectedAsset = tempSelectedAsset;
|
|
|
|
// Check if all assets are selected in a group to toggle the group selection's icon
|
|
if (!selectedGroup.has(groupIndex)) {
|
|
const assetsInGroup = $assetsGroupByDate[groupIndex];
|
|
let selectedAssetsInGroupCount = 0;
|
|
|
|
assetsInGroup.forEach((asset) => {
|
|
if (selectedAsset.has(asset.id)) {
|
|
selectedAssetsInGroupCount++;
|
|
}
|
|
});
|
|
|
|
// Taking into account of assets in group that are already in album
|
|
if (groupWithAssetsInAlbum[groupIndex]) {
|
|
selectedAssetsInGroupCount += groupWithAssetsInAlbum[groupIndex].size;
|
|
}
|
|
|
|
// if all assets are selected in a group, add the group to selected group
|
|
if (selectedAssetsInGroupCount == assetsInGroup.length) {
|
|
selectedGroup = selectedGroup.add(groupIndex);
|
|
}
|
|
}
|
|
};
|
|
|
|
const selectAssetGroupHandler = (groupIndex: number) => {
|
|
if (existingGroup.has(groupIndex)) return;
|
|
|
|
let tempSelectedGroup = new Set(selectedGroup);
|
|
let tempSelectedAsset = new Set(selectedAsset);
|
|
|
|
if (selectedGroup.has(groupIndex)) {
|
|
tempSelectedGroup.delete(groupIndex);
|
|
tempSelectedAsset.forEach((assetId) => {
|
|
if ($assetsGroupByDate[groupIndex].find((a) => a.id == assetId)) {
|
|
tempSelectedAsset.delete(assetId);
|
|
}
|
|
});
|
|
} else {
|
|
tempSelectedGroup.add(groupIndex);
|
|
tempSelectedAsset = new Set([
|
|
...selectedAsset,
|
|
...$assetsGroupByDate[groupIndex].map((a) => a.id)
|
|
]);
|
|
}
|
|
|
|
// Remove existed assets in the date group
|
|
if (groupWithAssetsInAlbum[groupIndex]) {
|
|
tempSelectedAsset.forEach((assetId) => {
|
|
if (groupWithAssetsInAlbum[groupIndex].has(assetId)) {
|
|
tempSelectedAsset.delete(assetId);
|
|
}
|
|
});
|
|
}
|
|
|
|
selectedAsset = tempSelectedAsset;
|
|
selectedGroup = tempSelectedGroup;
|
|
};
|
|
|
|
const addSelectedAssets = async () => {
|
|
dispatch('create-album', {
|
|
assets: Array.from(selectedAsset)
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This function is used to scan for existing selected group in the album
|
|
* and format it into the form of Record<any, Set<string>> to conditionally render and perform interaction
|
|
* relationship between the noneselected assets/groups
|
|
* with the existing assets/groups
|
|
*/
|
|
const scanForExistingSelectedGroup = () => {
|
|
if (assetsInAlbum) {
|
|
// Convert to each assetGroup to set of assetIds
|
|
const distinctAssetGroup = $assetsGroupByDate.map((assetGroup) => {
|
|
return new Set(assetGroup.map((asset) => asset.id));
|
|
});
|
|
|
|
// Find the group that contains all existed assets with the same set of assetIds
|
|
for (const assetInAlbum of assetsInAlbum) {
|
|
distinctAssetGroup.forEach((group, index) => {
|
|
if (group.has(assetInAlbum.id)) {
|
|
groupWithAssetsInAlbum[index] = new Set(groupWithAssetsInAlbum[index] || []).add(
|
|
assetInAlbum.id
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Object.keys(groupWithAssetsInAlbum).forEach((key) => {
|
|
if (distinctAssetGroup[parseInt(key)].size == groupWithAssetsInAlbum[parseInt(key)].size) {
|
|
existingGroup = existingGroup.add(parseInt(key));
|
|
}
|
|
});
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<section
|
|
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
|
class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]"
|
|
>
|
|
<AlbumAppBar on:close-button-click={() => dispatch('go-back')}>
|
|
<svelte:fragment slot="leading">
|
|
{#if selectedAsset.size == 0}
|
|
<p class="text-lg">Add to album</p>
|
|
{:else}
|
|
<p class="text-lg">{selectedAsset.size} selected</p>
|
|
{/if}
|
|
</svelte:fragment>
|
|
|
|
<svelte:fragment slot="trailing">
|
|
<button
|
|
on:click={() => openFileUploadDialog(UploadType.ALBUM)}
|
|
class="text-immich-primary text-sm hover:bg-immich-primary/10 transition-all px-6 py-2 rounded-lg font-medium"
|
|
>
|
|
Select from computer
|
|
</button>
|
|
<button
|
|
disabled={selectedAsset.size === 0}
|
|
on:click={addSelectedAssets}
|
|
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
|
><span class="px-2">Done</span></button
|
|
>
|
|
</svelte:fragment>
|
|
</AlbumAppBar>
|
|
|
|
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
|
|
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
|
<!-- Asset Group By Date -->
|
|
<div class="flex flex-col">
|
|
<!-- Date group title -->
|
|
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
|
<span
|
|
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
|
out:fly={{ x: -24, duration: 200 }}
|
|
class="inline-block px-2 hover:cursor-pointer"
|
|
on:click={() => selectAssetGroupHandler(groupIndex)}
|
|
>
|
|
{#if selectedGroup.has(groupIndex)}
|
|
<CheckCircle size="24" color="#4250af" />
|
|
{:else if existingGroup.has(groupIndex)}
|
|
<CheckCircle size="24" color="#757575" />
|
|
{:else}
|
|
<CircleOutline size="24" color="#757575" />
|
|
{/if}
|
|
</span>
|
|
|
|
{moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')}
|
|
</p>
|
|
|
|
<!-- Image grid -->
|
|
<div class="flex flex-wrap gap-[2px]">
|
|
{#each assetsInDateGroup as asset}
|
|
<ImmichThumbnail
|
|
{asset}
|
|
on:click={() => selectAssetHandler(asset.id, groupIndex)}
|
|
{groupIndex}
|
|
selected={selectedAsset.has(asset.id)}
|
|
isExisted={assetsInAlbum.findIndex((a) => a.id == asset.id) != -1}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</section>
|
|
</section>
|