Sfoglia il codice sorgente

Added sharing page to web (#355)

* Added shared album

* Added list tile

* Show info of shared album owner
Alex 3 anni fa
parent
commit
c6ecfb679a
38 ha cambiato i file con 402 aggiunte e 62 eliminazioni
  1. 0 1
      mobile/openapi/.openapi-generator/FILES
  2. 1 0
      mobile/openapi/README.md
  3. 42 0
      mobile/openapi/doc/UserApi.md
  4. 48 0
      mobile/openapi/lib/api/user_api.dart
  5. 5 0
      server/apps/immich/src/api-v1/user/user.controller.ts
  6. 9 1
      server/apps/immich/src/api-v1/user/user.service.ts
  7. 0 0
      server/immich-openapi-specs.json
  8. 63 0
      web/src/api/open-api/api.ts
  9. 0 0
      web/src/lib/components/admin-page/user-management.svelte
  10. 0 0
      web/src/lib/components/album-page/album-card.svelte
  11. 9 3
      web/src/lib/components/album-page/album-viewer.svelte
  12. 4 2
      web/src/lib/components/asset-viewer-page/asser-viewer-nav-bar.svelte
  13. 0 0
      web/src/lib/components/asset-viewer-page/asset-viewer.svelte
  14. 0 0
      web/src/lib/components/asset-viewer-page/detail-panel.svelte
  15. 0 0
      web/src/lib/components/asset-viewer-page/download-panel.svelte
  16. 0 0
      web/src/lib/components/asset-viewer-page/intersection-observer.svelte
  17. 14 5
      web/src/lib/components/asset-viewer-page/photo-viewer.svelte
  18. 14 5
      web/src/lib/components/asset-viewer-page/video-viewer.svelte
  19. 0 0
      web/src/lib/components/shared-components/announcement-box.svelte
  20. 0 0
      web/src/lib/components/shared-components/circle-avatar.svelte
  21. 0 0
      web/src/lib/components/shared-components/circle-icon-button.svelte
  22. 0 0
      web/src/lib/components/shared-components/full-screen-modal.svelte
  23. 1 1
      web/src/lib/components/shared-components/immich-thumbnail.svelte
  24. 0 0
      web/src/lib/components/shared-components/loading-spinner.svelte
  25. 0 0
      web/src/lib/components/shared-components/navigation-bar.svelte
  26. 0 0
      web/src/lib/components/shared-components/side-bar/side-bar-button.svelte
  27. 13 19
      web/src/lib/components/shared-components/side-bar/side-bar.svelte
  28. 0 0
      web/src/lib/components/shared-components/status-box.svelte
  29. 0 0
      web/src/lib/components/shared-components/upload-panel.svelte
  30. 60 0
      web/src/lib/components/sharing-page/shared-album-list-tile.svelte
  31. 2 1
      web/src/lib/models/admin-sidebar-selection.ts
  32. 1 1
      web/src/lib/stores/assets.ts
  33. 9 5
      web/src/routes/__layout.svelte
  34. 8 8
      web/src/routes/admin/index.svelte
  35. 1 1
      web/src/routes/albums/[albumId].svelte
  36. 4 4
      web/src/routes/albums/index.svelte
  37. 5 5
      web/src/routes/photos/index.svelte
  38. 89 0
      web/src/routes/sharing/index.svelte

+ 0 - 1
mobile/openapi/.openapi-generator/FILES

@@ -99,4 +99,3 @@ lib/model/user_count_response_dto.dart
 lib/model/user_response_dto.dart
 lib/model/validate_access_token_response_dto.dart
 pubspec.yaml
-test/thumbnail_format_test.dart

+ 1 - 0
mobile/openapi/README.md

@@ -100,6 +100,7 @@ Class | Method | HTTP request | Description
 *UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | 
 *UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | 
 *UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
+*UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /user/{userId} | 
 *UserApi* | [**getUserCount**](doc//UserApi.md#getusercount) | **GET** /user/count | 
 *UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /user | 
 

+ 42 - 0
mobile/openapi/doc/UserApi.md

@@ -14,6 +14,7 @@ Method | HTTP request | Description
 [**getAllUsers**](UserApi.md#getallusers) | **GET** /user | 
 [**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | 
 [**getProfileImage**](UserApi.md#getprofileimage) | **GET** /user/profile-image/{userId} | 
+[**getUserById**](UserApi.md#getuserbyid) | **GET** /user/{userId} | 
 [**getUserCount**](UserApi.md#getusercount) | **GET** /user/count | 
 [**updateUser**](UserApi.md#updateuser) | **PUT** /user | 
 
@@ -243,6 +244,47 @@ No authorization required
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
+# **getUserById**
+> UserResponseDto getUserById(userId)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+
+final api_instance = UserApi();
+final userId = userId_example; // String | 
+
+try {
+    final result = api_instance.getUserById(userId);
+    print(result);
+} catch (e) {
+    print('Exception when calling UserApi->getUserById: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **userId** | **String**|  | 
+
+### Return type
+
+[**UserResponseDto**](UserResponseDto.md)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **getUserCount**
 > UserCountResponseDto getUserCount()
 

+ 48 - 0
mobile/openapi/lib/api/user_api.dart

@@ -261,6 +261,54 @@ class UserApi {
     return null;
   }
 
+  /// Performs an HTTP 'GET /user/{userId}' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] userId (required):
+  Future<Response> getUserByIdWithHttpInfo(String userId,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/user/{userId}'
+      .replaceAll('{userId}', userId);
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] userId (required):
+  Future<UserResponseDto?> getUserById(String userId,) async {
+    final response = await getUserByIdWithHttpInfo(userId,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
+    
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'GET /user/count' operation and returns the [Response].
   Future<Response> getUserCountWithHttpInfo() async {
     // ignore: prefer_const_declarations

+ 5 - 0
server/apps/immich/src/api-v1/user/user.controller.ts

@@ -45,6 +45,11 @@ export class UserController {
     return await this.userService.getAllUsers(authUser, isAll);
   }
 
+  @Get('/:userId')
+  async getUserById(@Param('userId') userId: string): Promise<UserResponseDto> {
+    return await this.userService.getUserById(userId);
+  }
+
   @UseGuards(JwtAuthGuard)
   @ApiBearerAuth()
   @Get('me')

+ 9 - 1
server/apps/immich/src/api-v1/user/user.service.ts

@@ -38,6 +38,14 @@ export class UserService {
     return allUserExceptRequestedUser.map(mapUser);
   }
 
+  async getUserById(userId: string): Promise<UserResponseDto> {
+    const user = await this.userRepository.get(userId);
+    if (!user) {
+      throw new NotFoundException('User not found');
+    }
+
+    return mapUser(user);
+  }
   async getUserInfo(authUser: AuthUserDto): Promise<UserResponseDto> {
     const user = await this.userRepository.get(authUser.id);
     if (!user) {
@@ -94,7 +102,7 @@ export class UserService {
     }
 
     try {
-      await this.userRepository.createProfileImage(user, fileInfo)
+      await this.userRepository.createProfileImage(user, fileInfo);
 
       return mapCreateProfileImageResponse(authUser.id, fileInfo.path);
     } catch (e) {

File diff suppressed because it is too large
+ 0 - 0
server/immich-openapi-specs.json


+ 63 - 0
web/src/api/open-api/api.ts

@@ -3605,6 +3605,39 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
 
 
     
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getUserById: async (userId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'userId' is not null or undefined
+            assertParamExists('getUserById', 'userId', userId)
+            const localVarPath = `/user/{userId}`
+                .replace(`{${"userId"}}`, encodeURIComponent(String(userId)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -3741,6 +3774,16 @@ export const UserApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getProfileImage(userId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getUserById(userId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserById(userId, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -3814,6 +3857,15 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
         getProfileImage(userId: string, options?: any): AxiosPromise<object> {
             return localVarFp.getProfileImage(userId, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {string} userId 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getUserById(userId: string, options?: any): AxiosPromise<UserResponseDto> {
+            return localVarFp.getUserById(userId, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -3895,6 +3947,17 @@ export class UserApi extends BaseAPI {
         return UserApiFp(this.configuration).getProfileImage(userId, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {string} userId 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof UserApi
+     */
+    public getUserById(userId: string, options?: AxiosRequestConfig) {
+        return UserApiFp(this.configuration).getUserById(userId, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.

+ 0 - 0
web/src/lib/components/admin/user-management.svelte → web/src/lib/components/admin-page/user-management.svelte


+ 0 - 0
web/src/lib/components/album/album-card.svelte → web/src/lib/components/album-page/album-card.svelte


+ 9 - 3
web/src/lib/components/album/album-viewer.svelte → web/src/lib/components/album-page/album-viewer.svelte

@@ -1,17 +1,23 @@
 <script lang="ts">
+	import { afterNavigate } from '$app/navigation';
+
 	import { AlbumResponseDto, ThumbnailFormat } from '@api';
 	import { createEventDispatcher, onMount } from 'svelte';
 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
-	import CircleAvatar from '../shared/circle-avatar.svelte';
-	import ImmichThumbnail from '../shared/immich-thumbnail.svelte';
+	import CircleAvatar from '../shared-components/circle-avatar.svelte';
+	import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
 
 	const dispatch = createEventDispatcher();
 	export let album: AlbumResponseDto;
 	let viewWidth: number;
 	let thumbnailSize: number = 300;
 	let border = '';
+	let backUrl = '/albums';
 
+	afterNavigate(({ from, to }) => {
+		backUrl = from?.pathname ?? '/albums';
+	});
 	$: {
 		if (album.assets.length < 6) {
 			thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length);
@@ -51,7 +57,7 @@
 <section class="w-screen h-screen bg-immich-bg">
 	<div class="fixed top-0 w-full bg-immich-bg z-[100]">
 		<div class={`flex justify-between rounded-lg ${border} p-2 mx-2 mt-2 transition-all`}>
-			<a sveltekit:prefetch href="/albums" title="Go Back">
+			<a sveltekit:prefetch href={backUrl} title="Go Back">
 				<button
 					id="immich-circle-icon-button"
 					class={`rounded-full p-3 flex place-items-center place-content-center text-gray-600 transition-all hover:bg-gray-200`}

+ 4 - 2
web/src/lib/components/asset-viewer/asser-viewer-nav-bar.svelte → web/src/lib/components/asset-viewer-page/asser-viewer-nav-bar.svelte

@@ -6,11 +6,13 @@
 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
 	import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
 	import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
-	import CircleIconButton from '../shared/circle-icon-button.svelte';
+	import CircleIconButton from '../shared-components/circle-icon-button.svelte';
 	const dispatch = createEventDispatcher();
 </script>
 
-<div class="h-16 bg-black/5 flex justify-between place-items-center px-3 transition-transform duration-200 z-[9999]">
+<div
+	class="h-16 bg-black/5 flex justify-between place-items-center px-3 transition-transform duration-200 z-[9999]"
+>
 	<div>
 		<CircleIconButton logo={ArrowLeft} on:click={() => dispatch('goBack')} />
 	</div>

+ 0 - 0
web/src/lib/components/asset-viewer/asset-viewer.svelte → web/src/lib/components/asset-viewer-page/asset-viewer.svelte


+ 0 - 0
web/src/lib/components/asset-viewer/detail-panel.svelte → web/src/lib/components/asset-viewer-page/detail-panel.svelte


+ 0 - 0
web/src/lib/components/asset-viewer/download-panel.svelte → web/src/lib/components/asset-viewer-page/download-panel.svelte


+ 0 - 0
web/src/lib/components/asset-viewer/intersection-observer.svelte → web/src/lib/components/asset-viewer-page/intersection-observer.svelte


+ 14 - 5
web/src/lib/components/asset-viewer/photo-viewer.svelte → web/src/lib/components/asset-viewer-page/photo-viewer.svelte

@@ -3,7 +3,7 @@
 	import { fade } from 'svelte/transition';
 
 	import { createEventDispatcher, onMount } from 'svelte';
-	import LoadingSpinner from '../shared/loading-spinner.svelte';
+	import LoadingSpinner from '../shared-components/loading-spinner.svelte';
 	import { api, AssetResponseDto } from '@api';
 
 	export let assetId: string;
@@ -23,9 +23,15 @@
 	const loadAssetData = async () => {
 		if ($session.user) {
 			try {
-				const { data } = await api.assetApi.serveFile(assetInfo.deviceAssetId, deviceId, false, true, {
-					responseType: 'blob',
-				});
+				const { data } = await api.assetApi.serveFile(
+					assetInfo.deviceAssetId,
+					deviceId,
+					false,
+					true,
+					{
+						responseType: 'blob'
+					}
+				);
 
 				if (!(data instanceof Blob)) {
 					return;
@@ -38,7 +44,10 @@
 	};
 </script>
 
-<div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
+<div
+	transition:fade={{ duration: 150 }}
+	class="flex place-items-center place-content-center h-full select-none"
+>
 	{#if assetInfo}
 		{#await loadAssetData()}
 			<LoadingSpinner />

+ 14 - 5
web/src/lib/components/asset-viewer/video-viewer.svelte → web/src/lib/components/asset-viewer-page/video-viewer.svelte

@@ -3,7 +3,7 @@
 	import { fade } from 'svelte/transition';
 
 	import { createEventDispatcher, onMount } from 'svelte';
-	import LoadingSpinner from '../shared/loading-spinner.svelte';
+	import LoadingSpinner from '../shared-components/loading-spinner.svelte';
 	import { api, AssetResponseDto } from '@api';
 
 	export let assetId: string;
@@ -30,9 +30,15 @@
 
 		if ($session.user) {
 			try {
-				const { data } = await api.assetApi.serveFile(asset.deviceAssetId, asset.deviceId, false, true, {
-					responseType: 'blob',
-				});
+				const { data } = await api.assetApi.serveFile(
+					asset.deviceAssetId,
+					asset.deviceId,
+					false,
+					true,
+					{
+						responseType: 'blob'
+					}
+				);
 
 				if (!(data instanceof Blob)) {
 					return;
@@ -57,7 +63,10 @@
 	};
 </script>
 
-<div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
+<div
+	transition:fade={{ duration: 150 }}
+	class="flex place-items-center place-content-center h-full select-none"
+>
 	{#if asset}
 		<video controls class="h-full object-contain" bind:this={videoPlayerNode}>
 			<track kind="captions" />

+ 0 - 0
web/src/lib/components/shared/announcement-box.svelte → web/src/lib/components/shared-components/announcement-box.svelte


+ 0 - 0
web/src/lib/components/shared/circle-avatar.svelte → web/src/lib/components/shared-components/circle-avatar.svelte


+ 0 - 0
web/src/lib/components/shared/circle-icon-button.svelte → web/src/lib/components/shared-components/circle-icon-button.svelte


+ 0 - 0
web/src/lib/components/shared/full-screen-modal.svelte → web/src/lib/components/shared-components/full-screen-modal.svelte


+ 1 - 1
web/src/lib/components/shared/immich-thumbnail.svelte → web/src/lib/components/shared-components/immich-thumbnail.svelte

@@ -2,7 +2,7 @@
 	import { session } from '$app/stores';
 	import { createEventDispatcher, onDestroy } from 'svelte';
 	import { fade, fly } from 'svelte/transition';
-	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
+	import IntersectionObserver from '$lib/components/asset-viewer-page/intersection-observer.svelte';
 	import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
 	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
 	import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';

+ 0 - 0
web/src/lib/components/shared/loading-spinner.svelte → web/src/lib/components/shared-components/loading-spinner.svelte


+ 0 - 0
web/src/lib/components/shared/navigation-bar.svelte → web/src/lib/components/shared-components/navigation-bar.svelte


+ 0 - 0
web/src/lib/components/shared/side-bar/side-bar-button.svelte → web/src/lib/components/shared-components/side-bar/side-bar-button.svelte


+ 13 - 19
web/src/lib/components/shared/side-bar/side-bar.svelte → web/src/lib/components/shared-components/side-bar/side-bar.svelte

@@ -6,37 +6,24 @@
 	import { page } from '$app/stores';
 	import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
+	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 	import SideBarButton from './side-bar-button.svelte';
 	import StatusBox from '../status-box.svelte';
 
 	let selectedAction: AppSideBarSelection;
 
-	const onSidebarButtonClicked = (buttonType: CustomEvent) => {
-		selectedAction = buttonType.detail['actionType'] as AppSideBarSelection;
-
-		if (selectedAction == AppSideBarSelection.PHOTOS) {
-			if ($page.routeId != 'photos') {
-				goto('/photos');
-			}
-		}
-
-		if (selectedAction == AppSideBarSelection.ALBUMS) {
-			if ($page.routeId != 'albums') {
-				goto('/albums');
-			}
-		}
-	};
-
 	onMount(async () => {
 		if ($page.routeId == 'albums') {
 			selectedAction = AppSideBarSelection.ALBUMS;
 		} else if ($page.routeId == 'photos') {
 			selectedAction = AppSideBarSelection.PHOTOS;
+		} else if ($page.routeId == 'sharing') {
+			selectedAction = AppSideBarSelection.SHARING;
 		}
 	});
 </script>
 
-<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
+<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
 	<a sveltekit:prefetch href={$page.routeId != 'photos' ? `/photos` : null}>
 		<SideBarButton
 			title="Photos"
@@ -45,8 +32,15 @@
 			isSelected={selectedAction === AppSideBarSelection.PHOTOS}
 		/></a
 	>
-
-	<div class="text-xs ml-5">
+	<a sveltekit:prefetch href={$page.routeId != 'sharing' ? `/sharing` : null}>
+		<SideBarButton
+			title="Sharing"
+			logo={AccountMultipleOutline}
+			actionType={AppSideBarSelection.SHARING}
+			isSelected={selectedAction === AppSideBarSelection.SHARING}
+		/></a
+	>
+	<div class="text-xs ml-5 my-4">
 		<p>LIBRARY</p>
 	</div>
 	<a sveltekit:prefetch href={$page.routeId != 'albums' ? `/albums` : null}>

+ 0 - 0
web/src/lib/components/shared/status-box.svelte → web/src/lib/components/shared-components/status-box.svelte


+ 0 - 0
web/src/lib/components/shared/upload-panel.svelte → web/src/lib/components/shared-components/upload-panel.svelte


+ 60 - 0
web/src/lib/components/sharing-page/shared-album-list-tile.svelte

@@ -0,0 +1,60 @@
+<script lang="ts">
+	import { AlbumResponseDto, api, ThumbnailFormat, UserResponseDto } from '@api';
+	import { onMount } from 'svelte';
+	import { fade } from 'svelte/transition';
+
+	export let album: AlbumResponseDto;
+	export let user: UserResponseDto;
+
+	const loadImageData = async (thubmnailId: string | null) => {
+		if (thubmnailId == null) {
+			return '/no-thumbnail.png';
+		}
+
+		const { data } = await api.assetApi.getAssetThumbnail(thubmnailId!, ThumbnailFormat.Webp, {
+			responseType: 'blob'
+		});
+		if (data instanceof Blob) {
+			return URL.createObjectURL(data);
+		}
+	};
+
+	const getAlbumOwnerInfo = async (): Promise<UserResponseDto> => {
+		const { data } = await api.userApi.getUserById(album.ownerId);
+
+		return data;
+	};
+</script>
+
+<div
+	class="flex min-w-[550px] border-b border-gray-300 place-items-center py-4  gap-6 transition-all hover:border-immich-primary"
+>
+	<div>
+		{#await loadImageData(album.albumThumbnailAssetId)}
+			<div
+				class={`bg-immich-primary/10 w-[75px] h-[75px] flex place-items-center place-content-center rounded-xl`}
+			>
+				...
+			</div>
+		{:then imageData}
+			<img
+				in:fade={{ duration: 250 }}
+				src={imageData}
+				alt={album.id}
+				class={`object-cover w-[75px] h-[75px] transition-all z-0 rounded-xl duration-300 `}
+			/>
+		{/await}
+	</div>
+
+	<div>
+		<p class="font-medium text-gray-800">{album.albumName}</p>
+
+		{#await getAlbumOwnerInfo() then albumOwner}
+			{#if user.email == albumOwner.email}
+				<p class="text-xs text-gray-600">Owned</p>
+			{:else}
+				<p class="text-xs text-gray-600">Shared by {albumOwner.email}</p>
+			{/if}
+		{/await}
+	</div>
+</div>

+ 2 - 1
web/src/lib/models/admin-sidebar-selection.ts

@@ -1,9 +1,10 @@
 export enum AdminSideBarSelection {
-	USER_MANAGEMENT = 'User management',
+	USER_MANAGEMENT = 'User management'
 }
 
 export enum AppSideBarSelection {
 	PHOTOS = 'Photos',
 	EXPLORE = 'Explore',
 	ALBUMS = 'Albums',
+	SHARING = 'Sharing'
 }

+ 1 - 1
web/src/lib/stores/assets.ts

@@ -21,7 +21,7 @@ export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupB
 	return $assetsGroupByDate.flat();
 });
 
-export const getAssetsInfo = async (accessToken: string) => {
+export const getAssetsInfo = async () => {
 	const { data } = await api.assetApi.getAllAssets();
 	assets.set(data);
 };

+ 9 - 5
web/src/routes/__layout.svelte

@@ -8,7 +8,7 @@
 		}
 
 		return {
-			props: { url },
+			props: { url }
 		};
 	};
 </script>
@@ -18,9 +18,9 @@
 
 	import { blur, fade, slide } from 'svelte/transition';
 
-	import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
-	import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
-	import UploadPanel from '$lib/components/shared/upload-panel.svelte';
+	import DownloadPanel from '$lib/components/asset-viewer-page/download-panel.svelte';
+	import AnnouncementBox from '$lib/components/shared-components/announcement-box.svelte';
+	import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
 	import { onMount } from 'svelte';
 	import { api } from '@api';
 
@@ -45,7 +45,11 @@
 			<DownloadPanel />
 			<UploadPanel />
 			{#if shouldShowAnnouncement}
-				<AnnouncementBox {localVersion} {remoteVersion} on:close={() => (shouldShowAnnouncement = false)} />
+				<AnnouncementBox
+					{localVersion}
+					{remoteVersion}
+					on:close={() => (shouldShowAnnouncement = false)}
+				/>
 			{/if}
 		</div>
 	{/key}

+ 8 - 8
web/src/routes/admin/index.svelte

@@ -6,7 +6,7 @@
 		if (!session.user) {
 			return {
 				status: 302,
-				redirect: '/auth/login',
+				redirect: '/auth/login'
 			};
 		}
 
@@ -16,8 +16,8 @@
 			status: 200,
 			props: {
 				user: session.user,
-				allUsers: data,
-			},
+				allUsers: data
+			}
 		};
 	};
 </script>
@@ -27,13 +27,13 @@
 
 	import type { ImmichUser } from '$lib/models/immich-user';
 	import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
-	import SideBarButton from '$lib/components/shared/side-bar/side-bar-button.svelte';
+	import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
 	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
-	import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
-	import UserManagement from '$lib/components/admin/user-management.svelte';
-	import FullScreenModal from '$lib/components/shared/full-screen-modal.svelte';
+	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
+	import UserManagement from '$lib/components/admin-page/user-management.svelte';
+	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 	import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
-	import StatusBox from '$lib/components/shared/status-box.svelte';
+	import StatusBox from '$lib/components/shared-components/status-box.svelte';
 
 	let selectedAction: AdminSideBarSelection;
 

+ 1 - 1
web/src/routes/albums/[albumId].svelte

@@ -37,7 +37,7 @@
 <script lang="ts">
 	import { goto } from '$app/navigation';
 
-	import AlbumViewer from '$lib/components/album/album-viewer.svelte';
+	import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
 
 	export let album: AlbumResponseDto;
 </script>

+ 4 - 4
web/src/routes/albums/index.svelte

@@ -3,10 +3,10 @@
 
 	import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
 
-	import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
+	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
 	import { ImmichUser } from '$lib/models/immich-user';
 	import type { Load } from '@sveltejs/kit';
-	import SideBar from '$lib/components/shared/side-bar/side-bar.svelte';
+	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
 	import { AlbumResponseDto, api } from '@api';
 
 	export const load: Load = async ({ session }) => {
@@ -36,7 +36,7 @@
 </script>
 
 <script lang="ts">
-	import AlbumCard from '$lib/components/album/album-card.svelte';
+	import AlbumCard from '$lib/components/album-page/album-card.svelte';
 	import { goto } from '$app/navigation';
 
 	export let user: ImmichUser;
@@ -64,7 +64,7 @@
 		<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
 			<div class="px-4 flex justify-between place-items-center">
 				<div>
-					<p>Albums</p>
+					<p class="font-medium">Albums</p>
 				</div>
 
 				<div>

+ 5 - 5
web/src/routes/photos/index.svelte

@@ -12,7 +12,7 @@
 			};
 		}
 
-		await getAssetsInfo(session.user.accessToken);
+		await getAssetsInfo();
 
 		return {
 			status: 200,
@@ -26,17 +26,17 @@
 <script lang="ts">
 	import type { ImmichUser } from '$lib/models/immich-user';
 
-	import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
+	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
 	import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
 	import { fly } from 'svelte/transition';
 	import { session } from '$app/stores';
 	import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
-	import ImmichThumbnail from '$lib/components/shared/immich-thumbnail.svelte';
+	import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
 	import moment from 'moment';
-	import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
+	import AssetViewer from '$lib/components/asset-viewer-page/asset-viewer.svelte';
 	import { fileUploader } from '$lib/utils/file-uploader';
 	import { AssetResponseDto } from '@api';
-	import SideBar from '$lib/components/shared/side-bar/side-bar.svelte';
+	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
 
 	export let user: ImmichUser;
 

+ 89 - 0
web/src/routes/sharing/index.svelte

@@ -0,0 +1,89 @@
+<script context="module" lang="ts">
+	export const prerender = false;
+
+	import type { Load } from '@sveltejs/kit';
+	import { AlbumResponseDto, api, UserResponseDto } from '@api';
+
+	export const load: Load = async ({ session }) => {
+		if (!session.user) {
+			return {
+				status: 302,
+				redirect: '/auth/login'
+			};
+		}
+
+		let sharedAlbums: AlbumResponseDto[] = [];
+		try {
+			const { data } = await api.albumApi.getAllAlbums(true);
+			sharedAlbums = data;
+		} catch (e) {
+			console.log('Error [getAllAlbums] ', e);
+		}
+
+		return {
+			status: 200,
+			props: {
+				user: session.user,
+				sharedAlbums: sharedAlbums
+			}
+		};
+	};
+</script>
+
+<script lang="ts">
+	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
+	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
+	import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
+	import AlbumCard from '$lib/components/album-page/album-card.svelte';
+	import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
+
+	export let user: UserResponseDto;
+	export let sharedAlbums: AlbumResponseDto[];
+</script>
+
+<svelte:head>
+	<title>Albums - Immich</title>
+</svelte:head>
+
+<section>
+	<NavigationBar {user} on:uploadClicked={() => {}} />
+</section>
+
+<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
+	<SideBar />
+
+	<section class="overflow-y-auto relative">
+		<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
+			<!-- Main Section -->
+			<div class="px-4 flex justify-between place-items-center">
+				<div>
+					<p class="font-medium">Sharing</p>
+				</div>
+
+				<div>
+					<button
+						class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
+					>
+						<span>
+							<PlusBoxOutline size="18" />
+						</span>
+						<p>Create shared album</p>
+					</button>
+				</div>
+			</div>
+
+			<div class="my-4">
+				<hr />
+			</div>
+
+			<!-- Share Album List -->
+			<div class="w-full flex flex-col place-items-center">
+				{#each sharedAlbums as album}
+					<a sveltekit:prefetch href={`albums/${album.id}`}>
+						<SharedAlbumListTile {album} {user} /></a
+					>
+				{/each}
+			</div>
+		</section>
+	</section>
+</section>

Some files were not shown because too many files changed in this diff