mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
Feature - Add upload functionality on Web (#231)
* Added file selector * Extract metadata to upload files to the web * Added request for uploading * Generate jpeg/Webp thumbnail for asset uploaded without thumbnail data * Added generating thumbnail for video and WebSocket broadcast after thumbnail is generated * Added video length extraction * Added Uploading Panel * Added upload progress store and styling the uploaded asset * Added condition to only show upload panel when there is upload in progress * Remove asset from the upload list after successfully uploading * Added WebSocket to listen to upload event on the web * Added mechanism to check for existing assets before uploading on the web * Added test workflow * Update readme
This commit is contained in:
@@ -91,7 +91,7 @@
|
||||
<Close size="24" color="#232323" />
|
||||
</button>
|
||||
|
||||
<p class="text-black text-lg">Info</p>
|
||||
<p class="text-immich-fg text-lg">Info</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4">
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { ImmichUser } from '$lib/models/immich-user';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fade, fly, slide } from 'svelte/transition';
|
||||
import { postRequest } from '../../api';
|
||||
import { serverEndpoint } from '../../constants';
|
||||
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
|
||||
import { clickOutside } from './click-outside';
|
||||
|
||||
export let user: ImmichUser;
|
||||
|
||||
let shouldShowAccountInfo = false;
|
||||
let shouldShowProfileImage = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let shouldShowAccountInfoPanel = false;
|
||||
onMount(async () => {
|
||||
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
|
||||
@@ -41,7 +44,7 @@
|
||||
</script>
|
||||
|
||||
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
||||
<div class="flex border place-items-center px-6 py-2 ">
|
||||
<div class="flex border-b place-items-center px-6 py-2 ">
|
||||
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
||||
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
|
||||
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
||||
@@ -49,13 +52,21 @@
|
||||
<div class="flex-1 ml-24">
|
||||
<input class="w-[50%] border rounded-md bg-gray-200 px-8 py-4" placeholder="Search - Coming soon" />
|
||||
</div>
|
||||
|
||||
<section class="flex gap-6 place-items-center">
|
||||
<!-- <div>Upload</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"
|
||||
>
|
||||
<TrayArrowUp size="20" />
|
||||
<span> Upload </span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if user.isAdmin}
|
||||
<button
|
||||
class={`hover:text-immich-primary font-medium ${
|
||||
class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium ${
|
||||
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
||||
}`}
|
||||
on:click={navigateToAdmin}>Administration</button
|
||||
|
||||
191
web/src/lib/components/shared/upload-panel.svelte
Normal file
191
web/src/lib/components/shared/upload-panel.svelte
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts">
|
||||
import { quartInOut } from 'svelte/easing';
|
||||
import { scale, fade } from 'svelte/transition';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte';
|
||||
import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
|
||||
import type { UploadAsset } from '$lib/models/upload-asset';
|
||||
|
||||
let showDetail = true;
|
||||
|
||||
let uploadLength = 0;
|
||||
|
||||
const showUploadImageThumbnail = async (a: UploadAsset) => {
|
||||
const extension = a.fileExtension.toLowerCase();
|
||||
|
||||
if (extension == 'jpeg' || extension == 'jpg' || extension == 'png') {
|
||||
try {
|
||||
const imgData = await a.file.arrayBuffer();
|
||||
const arrayBufferView = new Uint8Array(imgData);
|
||||
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' });
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
const imageUrl = urlCreator.createObjectURL(blob);
|
||||
const img: any = document.getElementById(`${a.id}`);
|
||||
img.src = imageUrl;
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
function getSizeInHumanReadableFormat(sizeInByte: number) {
|
||||
const pepibyte = 1.126 * Math.pow(10, 15);
|
||||
const tebibyte = 1.1 * Math.pow(10, 12);
|
||||
const gibibyte = 1.074 * Math.pow(10, 9);
|
||||
const mebibyte = 1.049 * Math.pow(10, 6);
|
||||
const kibibyte = 1024;
|
||||
// Pebibyte
|
||||
if (sizeInByte >= pepibyte) {
|
||||
// Pe
|
||||
return `${(sizeInByte / pepibyte).toFixed(1)}PB`;
|
||||
} else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) {
|
||||
// Te
|
||||
return `${(sizeInByte / tebibyte).toFixed(1)}TB`;
|
||||
} else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) {
|
||||
// Gi
|
||||
return `${(sizeInByte / gibibyte).toFixed(1)}GB`;
|
||||
} else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) {
|
||||
// Mega
|
||||
return `${(sizeInByte / mebibyte).toFixed(1)}MB`;
|
||||
} else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) {
|
||||
// Kibi
|
||||
return `${(sizeInByte / kibibyte).toFixed(1)}KB`;
|
||||
} else {
|
||||
return `${sizeInByte}B`;
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list
|
||||
$: {
|
||||
if ($uploadAssetsStore.length != uploadLength) {
|
||||
$uploadAssetsStore.map((asset) => {
|
||||
showUploadImageThumbnail(asset);
|
||||
});
|
||||
|
||||
uploadLength = $uploadAssetsStore.length;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (showDetail) {
|
||||
$uploadAssetsStore.map((asset) => {
|
||||
showUploadImageThumbnail(asset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let isUploading = false;
|
||||
|
||||
uploadAssetsStore.isUploading.subscribe((value) => (isUploading = value));
|
||||
</script>
|
||||
|
||||
{#if isUploading}
|
||||
<div
|
||||
in:fade={{ duration: 250 }}
|
||||
out:fade={{ duration: 250, delay: 1000 }}
|
||||
class="absolute right-6 bottom-6 z-[10000]"
|
||||
>
|
||||
{#if showDetail}
|
||||
<div
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
class="bg-gray-200 p-4 text-sm w-[300px] rounded-lg shadow-sm border "
|
||||
>
|
||||
<div class="flex justify-between place-item-center mb-4">
|
||||
<p class="text-xs text-gray-500">UPLOADING {$uploadAssetsStore.length}</p>
|
||||
<button
|
||||
on:click={() => (showDetail = false)}
|
||||
class="w-[20px] h-[20px] bg-gray-50 rounded-full flex place-items-center place-content-center transition-colors hover:bg-gray-100"
|
||||
>
|
||||
<WindowMinimize />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="upload-item-list" class="max-h-[400px] overflow-y-auto pr-2 rounded-lg">
|
||||
{#each $uploadAssetsStore as uploadAsset}
|
||||
<div
|
||||
in:fade={{ duration: 250 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
|
||||
>
|
||||
<div class="relative">
|
||||
<img
|
||||
in:fade={{ duration: 250 }}
|
||||
id={`${uploadAsset.id}`}
|
||||
src="/immich-logo.svg"
|
||||
alt=""
|
||||
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
|
||||
/>
|
||||
|
||||
<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
|
||||
<p
|
||||
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
|
||||
>
|
||||
.{uploadAsset.fileExtension}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2 pr-4 flex flex-col justify-between">
|
||||
<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}`}
|
||||
/>
|
||||
|
||||
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-full">
|
||||
<button
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
on:click={() => (showDetail = true)}
|
||||
class="absolute -top-4 -left-4 text-xs rounded-full w-10 h-10 p-5 flex place-items-center place-content-center bg-immich-primary text-gray-200"
|
||||
>
|
||||
{$uploadAssetsStore.length}
|
||||
</button>
|
||||
<button
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
on:click={() => (showDetail = true)}
|
||||
class="bg-gray-300 p-5 rounded-full w-16 h-16 flex place-items-center place-content-center text-sm shadow-lg "
|
||||
>
|
||||
<div class="animate-pulse">
|
||||
<CloudUploadOutline size="30" color="#4250af" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/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>
|
||||
6
web/src/lib/models/upload-asset.ts
Normal file
6
web/src/lib/models/upload-asset.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type UploadAsset = {
|
||||
id: string;
|
||||
file: File;
|
||||
progress: number;
|
||||
fileExtension: string;
|
||||
};
|
||||
@@ -1,33 +1,29 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { getRequest } from '$lib/api';
|
||||
import type { ImmichAsset } from '$lib/models/immich-asset'
|
||||
import type { ImmichAsset } from '$lib/models/immich-asset';
|
||||
import lodash from 'lodash-es';
|
||||
import moment from 'moment';
|
||||
|
||||
export const assets = writable<ImmichAsset[]>([]);
|
||||
|
||||
export const assetsGroupByDate = derived(assets, ($assets) => {
|
||||
|
||||
try {
|
||||
return lodash.chain($assets)
|
||||
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
|
||||
.sortBy((group) => $assets.indexOf(group[0]))
|
||||
.value();
|
||||
} catch (e) {
|
||||
console.log("error deriving state assets", e)
|
||||
return []
|
||||
}
|
||||
|
||||
})
|
||||
try {
|
||||
return lodash
|
||||
.chain($assets)
|
||||
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
|
||||
.sortBy((group) => $assets.indexOf(group[0]))
|
||||
.value();
|
||||
} catch (e) {
|
||||
console.log('error deriving state assets', e);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => {
|
||||
return $assetsGroupByDate.flat();
|
||||
})
|
||||
return $assetsGroupByDate.flat();
|
||||
});
|
||||
|
||||
export const getAssetsInfo = async (accessToken: string) => {
|
||||
const res = await getRequest('asset', accessToken);
|
||||
|
||||
assets.set(res);
|
||||
|
||||
}
|
||||
|
||||
const res = await getRequest('asset', accessToken);
|
||||
assets.set(res);
|
||||
};
|
||||
|
||||
45
web/src/lib/stores/upload.ts
Normal file
45
web/src/lib/stores/upload.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import type { UploadAsset } from '../models/upload-asset';
|
||||
|
||||
function createUploadStore() {
|
||||
const uploadAssets = writable<Array<UploadAsset>>([]);
|
||||
|
||||
const { subscribe } = uploadAssets;
|
||||
|
||||
const isUploading = derived(uploadAssets, ($uploadAssets) => {
|
||||
return $uploadAssets.length > 0 ? true : false;
|
||||
});
|
||||
|
||||
const addNewUploadAsset = (newAsset: UploadAsset) => {
|
||||
uploadAssets.update((currentSet) => [...currentSet, newAsset]);
|
||||
};
|
||||
|
||||
const updateProgress = (id: string, progress: number) => {
|
||||
uploadAssets.update((uploadingAssets) => {
|
||||
return uploadingAssets.map((asset) => {
|
||||
if (asset.id == id) {
|
||||
return {
|
||||
...asset,
|
||||
progress: progress,
|
||||
};
|
||||
}
|
||||
|
||||
return asset;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const removeUploadAsset = (id: string) => {
|
||||
uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id));
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
isUploading,
|
||||
addNewUploadAsset,
|
||||
updateProgress,
|
||||
removeUploadAsset,
|
||||
};
|
||||
}
|
||||
|
||||
export const uploadAssetsStore = createUploadStore();
|
||||
30
web/src/lib/stores/websocket.ts
Normal file
30
web/src/lib/stores/websocket.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Socket, io } from 'socket.io-client';
|
||||
import { serverEndpoint } from '../constants';
|
||||
import type { ImmichAsset } from '../models/immich-asset';
|
||||
import { assets } from './assets';
|
||||
|
||||
export const openWebsocketConnection = (accessToken: string) => {
|
||||
const websocket = io(serverEndpoint, {
|
||||
transports: ['polling'],
|
||||
reconnection: true,
|
||||
forceNew: true,
|
||||
autoConnect: true,
|
||||
extraHeaders: {
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
listenToEvent(websocket);
|
||||
};
|
||||
|
||||
const listenToEvent = (socket: Socket) => {
|
||||
socket.on('on_upload_success', (data) => {
|
||||
const newUploadedAsset: ImmichAsset = JSON.parse(data);
|
||||
|
||||
assets.update((assets) => [...assets, newUploadedAsset]);
|
||||
});
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.log('Websocket Error', e);
|
||||
});
|
||||
};
|
||||
113
web/src/lib/utils/file-uploader.ts
Normal file
113
web/src/lib/utils/file-uploader.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as exifr from 'exifr';
|
||||
import { serverEndpoint } from '../constants';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import type { UploadAsset } from '../models/upload-asset';
|
||||
|
||||
export async function fileUploader(asset: File, accessToken: string) {
|
||||
const assetType = asset.type.split('/')[0].toUpperCase();
|
||||
const temp = asset.name.split('.');
|
||||
const fileExtension = temp[temp.length - 1];
|
||||
const formData = new FormData();
|
||||
|
||||
try {
|
||||
let exifData = null;
|
||||
|
||||
if (assetType !== 'VIDEO') {
|
||||
exifData = await exifr.parse(asset);
|
||||
}
|
||||
|
||||
const createdAt =
|
||||
exifData && exifData.DateTimeOriginal != null
|
||||
? new Date(exifData.DateTimeOriginal).toISOString()
|
||||
: new Date(asset.lastModified).toISOString();
|
||||
|
||||
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||
|
||||
// Create and add Unique ID of asset on the device
|
||||
formData.append('deviceAssetId', deviceAssetId);
|
||||
|
||||
// Get device id - for web -> use WEB
|
||||
formData.append('deviceId', 'WEB');
|
||||
|
||||
// Get asset type
|
||||
formData.append('assetType', assetType);
|
||||
|
||||
// Get Asset Created Date
|
||||
formData.append('createdAt', createdAt);
|
||||
|
||||
// Get Asset Modified At
|
||||
formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
|
||||
|
||||
// Set Asset is Favorite to false
|
||||
formData.append('isFavorite', 'false');
|
||||
|
||||
// Get asset duration
|
||||
formData.append('duration', '0:00:00.000000');
|
||||
|
||||
// Get asset file extension
|
||||
formData.append('fileExtension', '.' + fileExtension);
|
||||
|
||||
// Get asset binary data.
|
||||
formData.append('assetData', asset);
|
||||
|
||||
// Check if asset upload on server before performing upload
|
||||
const res = await fetch(serverEndpoint + '/asset/check', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ deviceAssetId }),
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const { isExist } = await res.json();
|
||||
|
||||
if (isExist) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.upload.onloadstart = () => {
|
||||
const newUploadAsset: UploadAsset = {
|
||||
id: deviceAssetId,
|
||||
file: asset,
|
||||
progress: 0,
|
||||
fileExtension: fileExtension,
|
||||
};
|
||||
|
||||
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
|
||||
};
|
||||
|
||||
request.upload.onload = () => {
|
||||
setTimeout(() => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
// listen for `error` event
|
||||
request.upload.onerror = () => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
};
|
||||
|
||||
// listen for `abort` event
|
||||
request.upload.onabort = () => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
};
|
||||
|
||||
// listen for `progress` event
|
||||
request.upload.onprogress = (event) => {
|
||||
const percentComplete = Math.floor((event.loaded / event.total) * 100);
|
||||
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
|
||||
};
|
||||
|
||||
request.open('POST', `${serverEndpoint}/asset/upload`);
|
||||
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
request.send(formData);
|
||||
} catch (e) {
|
||||
console.log('error uploading file ', e);
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
import { blur } from 'svelte/transition';
|
||||
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import FullScreenModal from '../lib/components/shared/full-screen-modal.svelte';
|
||||
import AnnouncementBox from '../lib/components/shared/announcement-box.svelte';
|
||||
import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
|
||||
import UploadPanel from '$lib/components/shared/upload-panel.svelte';
|
||||
|
||||
export let url: string;
|
||||
export let shouldShowAnnouncement: boolean;
|
||||
@@ -36,7 +36,7 @@
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<slot />
|
||||
<DownloadPanel />
|
||||
|
||||
<UploadPanel />
|
||||
{#if shouldShowAnnouncement}
|
||||
<AnnouncementBox {localVersion} {remoteVersion} on:close={() => (shouldShowAnnouncement = false)} />
|
||||
{/if}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<script lang="ts">
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let isAdminUserExist: boolean;
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
|
||||
import StatusBox from '../../lib/components/shared/status-box.svelte';
|
||||
import { fileUploader } from '../../lib/utils/file-uploader';
|
||||
import { openWebsocketConnection } from '../../lib/stores/websocket';
|
||||
|
||||
export let user: ImmichUser;
|
||||
let selectedAction: AppSideBarSelection;
|
||||
@@ -64,6 +66,8 @@
|
||||
|
||||
if ($session.user) {
|
||||
await getAssetsInfo($session.user.accessToken);
|
||||
|
||||
openWebsocketConnection($session.user.accessToken);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,7 +83,34 @@
|
||||
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
||||
isShowAsset = true;
|
||||
// pushState(assetId);
|
||||
};
|
||||
|
||||
const uploadClickedHandler = async () => {
|
||||
if ($session.user) {
|
||||
try {
|
||||
let fileSelector = document.createElement('input');
|
||||
|
||||
fileSelector.type = 'file';
|
||||
fileSelector.multiple = true;
|
||||
fileSelector.accept = 'image/*,video/*,.heic,.heif';
|
||||
|
||||
fileSelector.onchange = async (e: any) => {
|
||||
const files = Array.from<File>(e.target.files);
|
||||
|
||||
const acceptedFile = files.filter(
|
||||
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image',
|
||||
);
|
||||
|
||||
for (const asset of acceptedFile) {
|
||||
await fileUploader(asset, $session.user!.accessToken);
|
||||
}
|
||||
};
|
||||
|
||||
fileSelector.click();
|
||||
} catch (e) {
|
||||
console.log('Error seelcting file', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -88,10 +119,10 @@
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<NavigationBar {user} />
|
||||
<NavigationBar {user} on:uploadClicked={uploadClickedHandler} />
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
|
||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||
<!-- Sidebar -->
|
||||
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
|
||||
<SideBarButton
|
||||
@@ -111,7 +142,7 @@
|
||||
|
||||
<!-- Main Section -->
|
||||
<section class="overflow-y-auto relative">
|
||||
<section id="assets-content" class="relative pt-8 pl-4">
|
||||
<section id="assets-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||
<section id="image-grid" class="flex flex-wrap gap-14">
|
||||
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
||||
<!-- Asset Group By Date -->
|
||||
@@ -121,7 +152,7 @@
|
||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<p class="font-medium text-sm text-black mb-2 flex place-items-center h-6">
|
||||
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
||||
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
|
||||
<div
|
||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||
@@ -136,7 +167,7 @@
|
||||
</p>
|
||||
|
||||
<!-- Image grid -->
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="flex flex-wrap gap-[2px]">
|
||||
{#each assetsInDateGroup as asset}
|
||||
<ImmichThumbnail
|
||||
{asset}
|
||||
|
||||
Reference in New Issue
Block a user