diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts index 477ea297e..a4bded295 100644 --- a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts @@ -18,8 +18,8 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ) { super({ jwtFromRequest: ExtractJwt.fromExtractors([ - immichJwtService.extractJwtFromHeader, immichJwtService.extractJwtFromCookie, + immichJwtService.extractJwtFromHeader, ]), ignoreExpiration: false, secretOrKey: jwtSecret, diff --git a/web/src/app.css b/web/src/app.css index 8b7432993..12b80cf28 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -6,26 +6,53 @@ @tailwind utilities; :root { - font-family: 'Work Sans', sans-serif; + font-family: 'Work Sans', sans-serif; } body { - min-height: 100vh; + /* min-height: 100vh; */ margin: 0; background-color: #f6f8fe; - color: #5f6368; + color: #5f6368; } @layer utilities { - .immich-form-input { - @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm - } + .immich-form-input { + @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm; + } - .immich-form-label { - @apply font-medium text-sm text-gray-500 - } + .immich-form-label { + @apply font-medium text-sm text-gray-500; + } - .immich-btn-primary { - @apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium - } -} \ No newline at end of file + .immich-btn-primary { + @apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium; + } + + .immich-text-button { + @apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium; + } + + /* width */ + .immich-scrollbar::-webkit-scrollbar { + width: 8px; + } + + /* Track */ + .immich-scrollbar::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 16px; + } + + /* Handle */ + .immich-scrollbar::-webkit-scrollbar-thumb { + background: rgba(85, 86, 87, 0.408); + border-radius: 16px; + } + + /* Handle on hover */ + .immich-scrollbar::-webkit-scrollbar-thumb:hover { + background: #4250afad; + border-radius: 16px; + } +} diff --git a/web/src/lib/components/album-page/album-app-bar.svelte b/web/src/lib/components/album-page/album-app-bar.svelte new file mode 100644 index 000000000..ba700eb26 --- /dev/null +++ b/web/src/lib/components/album-page/album-app-bar.svelte @@ -0,0 +1,39 @@ + + +
+
+
+ + +
+ +
+ +
+
+
diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index c78dd6490..b87c8dc3c 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,18 +1,30 @@ -
-
-
- + +
+ goto(backUrl)} backIcon={ArrowLeft}> + + {#if album.assets.length > 0} - -
- -
-
-
+ {/if} -
-

- {album.albumName} -

+ {#if isCreatingSharedAlbum && album.sharedUsers.length == 0} + + {/if} + + -

{getDateRange()}

+
+ (isEditingTitle = true)} + on:blur={() => (isEditingTitle = false)} + class={`transition-all text-6xl text-immich-primary w-[99%] border-b-2 border-transparent outline-none ${ + isOwned ? 'hover:border-gray-400' : 'hover:border-transparent' + } focus:outline-none focus:border-b-2 focus:border-immich-primary bg-immich-bg`} + type="text" + bind:value={album.albumName} + disabled={!isOwned} + /> - {#if album.sharedUsers.length > 0} -
+ {#if album.assets.length > 0} +

{getDateRange()}

+ {/if} + + {#if album.shared} +
{#each album.sharedUsers as user} {/each} + +
{/if} -
- {#each album.assets as asset} - {#if album.assets.length < 7} - - {:else} - - {/if} - {/each} -
+ {#if album.assets.length > 0} +
+ {#each album.assets as asset} + {#if album.assets.length < 7} + + {:else} + + {/if} + {/each} +
+ {:else} + +
+
+

ADD PHOTOS

+ +
+
+ {/if}
@@ -170,3 +289,19 @@ on:close={closeViewer} /> {/if} + +{#if isShowAssetSelection} + (isShowAssetSelection = false)} + on:create-album={createAlbumHandler} + /> +{/if} + +{#if isShowShareUserSelection} + (isShowShareUserSelection = false)} + on:add-user={addUserHandler} + sharedUsersInAlbum={new Set(album.sharedUsers)} + /> +{/if} diff --git a/web/src/lib/components/album-page/asset-selection.svelte b/web/src/lib/components/album-page/asset-selection.svelte new file mode 100644 index 000000000..362dc6f2d --- /dev/null +++ b/web/src/lib/components/album-page/asset-selection.svelte @@ -0,0 +1,197 @@ + + +
+ dispatch('go-back')}> + + {#if selectedAsset.size == 0} +

Add to album

+ {:else} +

{selectedAsset.size} selected

+ {/if} +
+ + + + +
+ +
+ {#each $assetsGroupByDate as assetsInDateGroup, groupIndex} + +
+ +

+ selectAssetGroupHandler(groupIndex)} + > + {#if selectedGroup.has(groupIndex)} + + {:else if existingGroup.has(groupIndex)} + + {:else} + + {/if} + + + {moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')} +

+ + +
+ {#each assetsInDateGroup as asset} + selectAssetHandler(asset.id, groupIndex)} + {groupIndex} + selected={selectedAsset.has(asset.id)} + isExisted={assetsInAlbum.findIndex((a) => a.id == asset.id) != -1} + /> + {/each} +
+
+ {/each} +
+
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte new file mode 100644 index 000000000..60cfd83bb --- /dev/null +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -0,0 +1,117 @@ + + + dispatch('close')}> + + + Immich +

Invite to album

+
+
+ +
+ {#if selectedUsers.size > 0} +
+

To

+ + {#each Array.from(selectedUsers) as user} + + {/each} +
+ {/if} + + {#if users.length > 0} +

SUGGESTIONS

+ +
+ {#each users as user} + + {/each} +
+ {:else} +

+ Looks like you have shared this album with all users or you don't have any user to share + with. +

+ {/if} + + {#if selectedUsers.size > 0} +
+ +
+ {/if} +
+
diff --git a/web/src/lib/components/shared-components/base-modal.svelte b/web/src/lib/components/shared-components/base-modal.svelte new file mode 100644 index 000000000..40297bfbb --- /dev/null +++ b/web/src/lib/components/shared-components/base-modal.svelte @@ -0,0 +1,31 @@ + + +
+
+
+
+ +

Modal Title

+
+
+ +
+ +
+ +
+
+
diff --git a/web/src/lib/components/shared-components/circle-avatar.svelte b/web/src/lib/components/shared-components/circle-avatar.svelte index 6afbcdeb5..1fea03910 100644 --- a/web/src/lib/components/shared-components/circle-avatar.svelte +++ b/web/src/lib/components/shared-components/circle-avatar.svelte @@ -1,9 +1,11 @@ {#await getUserAvatar()} -
+
{:then data} profile-img {/await} diff --git a/web/src/lib/components/shared-components/immich-thumbnail.svelte b/web/src/lib/components/shared-components/immich-thumbnail.svelte index 9f13c3da7..8917ffbe1 100644 --- a/web/src/lib/components/shared-components/immich-thumbnail.svelte +++ b/web/src/lib/components/shared-components/immich-thumbnail.svelte @@ -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 }); + } + };
dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })} + on:click={thumbnailClickedHandler} > - {#if mouseOver} + {#if mouseOver || selected || isExisted}
(mouseOverIcon = true)} on:mouseleave={() => (mouseOverIcon = false)} class="inline-block" > - + {#if selected} + + {:else if isExisted} + + {:else} + + {/if}
{/if} @@ -209,7 +247,7 @@
...
@@ -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} diff --git a/web/src/lib/components/shared-components/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar.svelte index 31582efc4..eab2a8343 100644 --- a/web/src/lib/components/shared-components/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar.svelte @@ -61,14 +61,17 @@

IMMICH

- +
{#if $page.url.pathname !== '/admin'}
- +
{/if} diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index d959ff706..038077dff 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -79,9 +79,7 @@ isUploading = value; if (isUploading == false) { - if ($session.user) { - getAssetsInfo($session.user.accessToken); - } + getAssetsInfo(); } }); @@ -107,7 +105,7 @@
-
+
{#each $uploadAssetsStore as uploadAsset}
@@ -144,7 +144,9 @@ class="bg-immich-primary h-[15px] rounded-md transition-all" style={`width: ${uploadAsset.progress}%`} /> -

{uploadAsset.progress}/100

+

+ {uploadAsset.progress}/100 +

@@ -173,28 +175,3 @@ {/if}
{/if} - - diff --git a/web/src/routes/albums/index.svelte b/web/src/routes/albums/index.svelte index 121f597d1..e15547b7d 100644 --- a/web/src/routes/albums/index.svelte +++ b/web/src/routes/albums/index.svelte @@ -17,10 +17,10 @@ }; } - let allAlbums: AlbumResponseDto[] = []; + let albums: AlbumResponseDto[] = []; try { const { data } = await api.albumApi.getAllAlbums(); - allAlbums = data; + albums = data; } catch (e) { console.log('Error [getAllAlbums] ', e); } @@ -29,7 +29,7 @@ status: 200, props: { user: session.user, - allAlbums: allAlbums + albums: albums } }; }; @@ -38,12 +38,47 @@ @@ -68,9 +103,7 @@
-
diff --git a/web/src/routes/photos/index.svelte b/web/src/routes/photos/index.svelte index 4ca379782..e2cc935c8 100644 --- a/web/src/routes/photos/index.svelte +++ b/web/src/routes/photos/index.svelte @@ -173,7 +173,7 @@ {/each} diff --git a/web/src/routes/sharing/index.svelte b/web/src/routes/sharing/index.svelte index 6013df5ca..4cfbf55fb 100644 --- a/web/src/routes/sharing/index.svelte +++ b/web/src/routes/sharing/index.svelte @@ -28,6 +28,28 @@ } }; }; + + const createSharedAlbum = async () => { + try { + const { data: newAlbum } = await api.albumApi.createAlbum({ + albumName: 'Untitled' + }); + + goto('/albums/' + newAlbum.id); + } catch (e) { + console.log('Error [createAlbum] ', e); + } + }; + + const deleteAlbum = async (album: AlbumResponseDto) => { + try { + await api.albumApi.deleteAlbum(album.id); + return true; + } catch (e) { + console.log('Error [deleteAlbum] ', e); + return false; + } + };