You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 04:38:12 +02:00
Implement album creation on web (#365)
* Added album creation button functionality * Added input for album title * Added select photos button * Added page to select assets * Show photo selection timeline * Implemented update album name mechanism: * Added selection mechanism * Added selection mechanism with existing assets in album * Refactored and added comments * Refactored and added comments - 2 * Refactor album app bar * Added modal for select user * Implemented choose users * Added additional share user button * Added rule to show add users button
This commit is contained in:
31
web/src/lib/components/shared-components/base-modal.svelte
Normal file
31
web/src/lib/components/shared-components/base-modal.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="immich-modal"
|
||||
transition:fly={{ y: 1000, duration: 200, easing: quintOut }}
|
||||
class="absolute top-0 w-screen h-screen z-[9999] bg-black/50 flex place-items-center place-content-center"
|
||||
>
|
||||
<div class="bg-white w-[450px] min-h-[200px] max-h-[500px] rounded-lg shadow-md">
|
||||
<div class="flex justify-between place-items-center p-5">
|
||||
<div>
|
||||
<slot name="title">
|
||||
<p>Modal Title</p>
|
||||
</slot>
|
||||
</div>
|
||||
<button on:click={() => dispatch('close')}>
|
||||
<Close size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
// Avatar Size In Pixel
|
||||
export let size: number = 48;
|
||||
|
||||
const getUserAvatar = async () => {
|
||||
try {
|
||||
const { data } = await api.userApi.getProfileImage(user.id, {
|
||||
@ -20,12 +22,18 @@
|
||||
</script>
|
||||
|
||||
{#await getUserAvatar()}
|
||||
<div class="w-12 h-12 rounded-full bg-immich-primary/25" />
|
||||
<div
|
||||
style:width={`${size}px`}
|
||||
style:height={`${size}px`}
|
||||
class={` rounded-full bg-immich-primary/25`}
|
||||
/>
|
||||
{:then data}
|
||||
<img
|
||||
src={data}
|
||||
alt="profile-img"
|
||||
class="inline rounded-full w-12 h-12 object-cover border shadow-md"
|
||||
style:width={`${size}px`}
|
||||
style:height={`${size}px`}
|
||||
class={`inline rounded-full object-cover border shadow-md`}
|
||||
title={user.email}
|
||||
/>
|
||||
{/await}
|
||||
|
@ -15,6 +15,8 @@
|
||||
export let groupIndex = 0;
|
||||
export let thumbnailSize: number | undefined = undefined;
|
||||
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
|
||||
export let selected: boolean = false;
|
||||
export let isExisted: boolean = false;
|
||||
|
||||
let imageData: string;
|
||||
let videoData: string;
|
||||
@ -27,6 +29,7 @@
|
||||
let isThumbnailVideoPlaying = false;
|
||||
let calculateVideoDurationIntervalHandler: NodeJS.Timer;
|
||||
let videoProgress = '00:00';
|
||||
let videoAbortController: AbortController;
|
||||
|
||||
const loadImageData = async () => {
|
||||
if ($session.user) {
|
||||
@ -42,52 +45,51 @@
|
||||
|
||||
const loadVideoData = async () => {
|
||||
isThumbnailVideoPlaying = false;
|
||||
videoAbortController = new AbortController();
|
||||
|
||||
if ($session.user) {
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
true,
|
||||
{
|
||||
responseType: 'blob'
|
||||
}
|
||||
);
|
||||
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
asset.deviceAssetId,
|
||||
asset.deviceId,
|
||||
false,
|
||||
true,
|
||||
{
|
||||
responseType: 'blob',
|
||||
signal: videoAbortController.signal
|
||||
}
|
||||
);
|
||||
|
||||
videoData = URL.createObjectURL(data);
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoPlayerNode.src = videoData;
|
||||
// videoPlayerNode.src = videoData + '#t=0,5';
|
||||
videoData = URL.createObjectURL(data);
|
||||
|
||||
videoPlayerNode.load();
|
||||
videoPlayerNode.src = videoData;
|
||||
|
||||
videoPlayerNode.onloadeddata = () => {
|
||||
console.log('first frame load');
|
||||
};
|
||||
videoPlayerNode.load();
|
||||
|
||||
videoPlayerNode.oncanplaythrough = () => {
|
||||
console.log('can play through');
|
||||
};
|
||||
videoPlayerNode.onloadeddata = () => {
|
||||
console.log('first frame load');
|
||||
};
|
||||
|
||||
videoPlayerNode.oncanplay = () => {
|
||||
console.log('can play');
|
||||
videoPlayerNode.muted = true;
|
||||
videoPlayerNode.play();
|
||||
videoPlayerNode.oncanplaythrough = () => {
|
||||
console.log('can play through');
|
||||
};
|
||||
|
||||
isThumbnailVideoPlaying = true;
|
||||
calculateVideoDurationIntervalHandler = setInterval(() => {
|
||||
videoProgress = getVideoDurationInString(Math.round(videoPlayerNode.currentTime));
|
||||
}, 1000);
|
||||
};
|
||||
videoPlayerNode.oncanplay = () => {
|
||||
console.log('can play');
|
||||
videoPlayerNode.muted = true;
|
||||
videoPlayerNode.play();
|
||||
|
||||
return videoData;
|
||||
} catch (e) {}
|
||||
}
|
||||
isThumbnailVideoPlaying = true;
|
||||
calculateVideoDurationIntervalHandler = setInterval(() => {
|
||||
videoProgress = getVideoDurationInString(Math.round(videoPlayerNode.currentTime));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return videoData;
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const getVideoDurationInString = (currentTime: number) => {
|
||||
@ -137,6 +139,11 @@
|
||||
|
||||
const handleMouseLeaveThumbnail = () => {
|
||||
mouseOver = false;
|
||||
|
||||
// Stop XHR download of video
|
||||
videoAbortController?.abort();
|
||||
|
||||
// Stop video playback
|
||||
URL.revokeObjectURL(videoData);
|
||||
|
||||
clearInterval(calculateVideoDurationIntervalHandler);
|
||||
@ -144,28 +151,59 @@
|
||||
isThumbnailVideoPlaying = false;
|
||||
videoProgress = '00:00';
|
||||
};
|
||||
|
||||
$: getThumbnailBorderStyle = () => {
|
||||
if (selected) {
|
||||
return 'border-[20px] border-immich-primary/20';
|
||||
} else if (isExisted) {
|
||||
return 'border-[20px] border-gray-300';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$: getOverlaySelectorIconStyle = () => {
|
||||
if (selected || isExisted) {
|
||||
return '';
|
||||
} else {
|
||||
return 'bg-gradient-to-b from-gray-800/50';
|
||||
}
|
||||
};
|
||||
const thumbnailClickedHandler = () => {
|
||||
if (!isExisted) {
|
||||
dispatch('click', { assetId: asset.id, deviceId: asset.deviceId });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<IntersectionObserver once={true} let:intersecting>
|
||||
<div
|
||||
style:width={`${thumbnailSize}px`}
|
||||
style:height={`${thumbnailSize}px`}
|
||||
class={`bg-gray-100 relative hover:cursor-pointer ${getSize()}`}
|
||||
class={`bg-gray-100 relative ${getSize()} ${
|
||||
isExisted ? 'cursor-not-allowed' : 'hover:cursor-pointer'
|
||||
}`}
|
||||
on:mouseenter={handleMouseOverThumbnail}
|
||||
on:mouseleave={handleMouseLeaveThumbnail}
|
||||
on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })}
|
||||
on:click={thumbnailClickedHandler}
|
||||
>
|
||||
{#if mouseOver}
|
||||
{#if mouseOver || selected || isExisted}
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="w-full bg-gradient-to-b from-gray-800/50 via-white/0 to-white/0 absolute p-2 z-10"
|
||||
class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
|
||||
>
|
||||
<div
|
||||
on:mouseenter={() => (mouseOverIcon = true)}
|
||||
on:mouseleave={() => (mouseOverIcon = false)}
|
||||
class="inline-block"
|
||||
>
|
||||
<CheckCircle size="24" color={mouseOverIcon ? 'white' : '#d8dadb'} />
|
||||
{#if selected}
|
||||
<CheckCircle size="24" color="#4250af" />
|
||||
{:else if isExisted}
|
||||
<CheckCircle size="24" color="#252525" />
|
||||
{:else}
|
||||
<CheckCircle size="24" color={mouseOverIcon ? 'white' : '#d8dadb'} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -209,7 +247,7 @@
|
||||
<div
|
||||
style:width={`${thumbnailSize}px`}
|
||||
style:height={`${thumbnailSize}px`}
|
||||
class={`bg-immich-primary/10 ${getSize()} flex place-items-center place-content-center`}
|
||||
class={`bg-immich-primary/10 ${getSize()} flex place-items-center place-content-center `}
|
||||
>
|
||||
...
|
||||
</div>
|
||||
@ -220,7 +258,7 @@
|
||||
in:fade={{ duration: 250 }}
|
||||
src={imageData}
|
||||
alt={asset.id}
|
||||
class={`object-cover ${getSize()} transition-all duration-100 z-0`}
|
||||
class={`object-cover ${getSize()} transition-all duration-100 z-0 ${getThumbnailBorderStyle()}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
{/await}
|
||||
|
@ -61,14 +61,17 @@
|
||||
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
||||
</a>
|
||||
<div class="flex-1 ml-24">
|
||||
<input class="w-[50%] border rounded-md bg-gray-200 px-8 py-4" placeholder="Search - Coming soon" />
|
||||
<input
|
||||
class="w-[50%] border rounded-md bg-gray-200 px-8 py-4"
|
||||
placeholder="Search - Coming soon"
|
||||
/>
|
||||
</div>
|
||||
<section class="flex gap-4 place-items-center">
|
||||
{#if $page.url.pathname !== '/admin'}
|
||||
<button
|
||||
in:fly={{ x: 50, duration: 250 }}
|
||||
on:click={() => dispatch('uploadClicked')}
|
||||
class="flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium"
|
||||
class="immich-text-button"
|
||||
>
|
||||
<TrayArrowUp size="20" />
|
||||
<span> Upload </span>
|
||||
@ -158,7 +161,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<button class="border rounded-3xl px-6 py-2 hover:bg-gray-50" on:click={logOut}>Sign Out</button>
|
||||
<button class="border rounded-3xl px-6 py-2 hover:bg-gray-50" on:click={logOut}
|
||||
>Sign Out</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -79,9 +79,7 @@
|
||||
isUploading = value;
|
||||
|
||||
if (isUploading == false) {
|
||||
if ($session.user) {
|
||||
getAssetsInfo($session.user.accessToken);
|
||||
}
|
||||
getAssetsInfo();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -107,7 +105,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="upload-item-list" class="max-h-[400px] overflow-y-auto pr-2 rounded-lg">
|
||||
<div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar">
|
||||
{#each $uploadAssetsStore as uploadAsset}
|
||||
<div
|
||||
in:fade={{ duration: 250 }}
|
||||
@ -136,7 +134,9 @@
|
||||
<input
|
||||
disabled
|
||||
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
|
||||
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
|
||||
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${
|
||||
uploadAsset.file.name
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
||||
@ -144,7 +144,9 @@
|
||||
class="bg-immich-primary h-[15px] rounded-md transition-all"
|
||||
style={`width: ${uploadAsset.progress}%`}
|
||||
/>
|
||||
<p class="absolute h-full w-full text-center top-0 text-[10px] ">{uploadAsset.progress}/100</p>
|
||||
<p class="absolute h-full w-full text-center top-0 text-[10px] ">
|
||||
{uploadAsset.progress}/100
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -173,28 +175,3 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* width */
|
||||
#upload-item-list::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
#upload-item-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
#upload-item-list::-webkit-scrollbar-thumb {
|
||||
background: #4250af68;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
#upload-item-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4250afad;
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user