Переглянути джерело

[WEB] Upload asset directly to album (#379)

* Added stores to get album assetId

* Upload assets and add to album

* Added comments

* resolve conflict when add assets from upload directly

* Filtered out duplicate asset before adding to the album
Alex 3 роки тому
батько
коміт
03457f5d32

+ 1 - 0
mobile/openapi/doc/CheckDuplicateAssetResponseDto.md

@@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **isExist** | **bool** |  | 
+**id** | **String** |  | [optional] 
 
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 

+ 20 - 3
mobile/openapi/lib/model/check_duplicate_asset_response_dto.dart

@@ -14,25 +14,41 @@ class CheckDuplicateAssetResponseDto {
   /// Returns a new [CheckDuplicateAssetResponseDto] instance.
   CheckDuplicateAssetResponseDto({
     required this.isExist,
+    this.id,
   });
 
   bool isExist;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? id;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is CheckDuplicateAssetResponseDto &&
-     other.isExist == isExist;
+     other.isExist == isExist &&
+     other.id == id;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
-    (isExist.hashCode);
+    (isExist.hashCode) +
+    (id == null ? 0 : id!.hashCode);
 
   @override
-  String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist]';
+  String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist, id=$id]';
 
   Map<String, dynamic> toJson() {
     final _json = <String, dynamic>{};
       _json[r'isExist'] = isExist;
+    if (id != null) {
+      _json[r'id'] = id;
+    } else {
+      _json[r'id'] = null;
+    }
     return _json;
   }
 
@@ -56,6 +72,7 @@ class CheckDuplicateAssetResponseDto {
 
       return CheckDuplicateAssetResponseDto(
         isExist: mapValueOfType<bool>(json, r'isExist')!,
+        id: mapValueOfType<String>(json, r'id'),
       );
     }
     return null;

+ 1 - 3
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -202,8 +202,6 @@ export class AssetController {
     @GetAuthUser() authUser: AuthUserDto,
     @Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto,
   ): Promise<CheckDuplicateAssetResponseDto> {
-    const res = await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
-
-    return new CheckDuplicateAssetResponseDto(res);
+    return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
   }
 }

+ 8 - 2
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -24,6 +24,7 @@ import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
 import { CreateAssetDto } from './dto/create-asset.dto';
 import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
+import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 
 const fileInfo = promisify(stat);
 
@@ -487,7 +488,10 @@ export class AssetService {
     return curatedObjects;
   }
 
-  async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto): Promise<boolean> {
+  async checkDuplicatedAsset(
+    authUser: AuthUserDto,
+    checkDuplicateAssetDto: CheckDuplicateAssetDto,
+  ): Promise<CheckDuplicateAssetResponseDto> {
     const res = await this.assetRepository.findOne({
       where: {
         deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
@@ -496,6 +500,8 @@ export class AssetService {
       },
     });
 
-    return res ? true : false;
+    const isDuplicated = res ? true : false;
+
+    return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
   }
 }

+ 3 - 1
server/apps/immich/src/api-v1/asset/response-dto/check-duplicate-asset-response.dto.ts

@@ -1,6 +1,8 @@
 export class CheckDuplicateAssetResponseDto {
-  constructor(isExist: boolean) {
+  constructor(isExist: boolean, id?: string) {
     this.isExist = isExist;
+    this.id = id;
   }
   isExist: boolean;
+  id?: string;
 }

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
server/immich-openapi-specs.json


Різницю між файлами не показано, бо вона завелика
+ 902 - 902
web/src/api/open-api/api.ts


+ 22 - 25
web/src/api/open-api/base.ts

@@ -5,29 +5,30 @@
  * Immich API
  *
  * The version of the OpenAPI document: 1.17.0
- *
+ * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * https://openapi-generator.tech
  * Do not edit the class manually.
  */
 
-import { Configuration } from './configuration';
+
+import { Configuration } from "./configuration";
 // Some imports not used depending on template conditions
 // @ts-ignore
 import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
 
-export const BASE_PATH = '/api'.replace(/\/+$/, '');
+export const BASE_PATH = "/api".replace(/\/+$/, "");
 
 /**
  *
  * @export
  */
 export const COLLECTION_FORMATS = {
-	csv: ',',
-	ssv: ' ',
-	tsv: '\t',
-	pipes: '|'
+    csv: ",",
+    ssv: " ",
+    tsv: "\t",
+    pipes: "|",
 };
 
 /**
@@ -36,8 +37,8 @@ export const COLLECTION_FORMATS = {
  * @interface RequestArgs
  */
 export interface RequestArgs {
-	url: string;
-	options: AxiosRequestConfig;
+    url: string;
+    options: AxiosRequestConfig;
 }
 
 /**
@@ -46,19 +47,15 @@ export interface RequestArgs {
  * @class BaseAPI
  */
 export class BaseAPI {
-	protected configuration: Configuration | undefined;
+    protected configuration: Configuration | undefined;
 
-	constructor(
-		configuration?: Configuration,
-		protected basePath: string = BASE_PATH,
-		protected axios: AxiosInstance = globalAxios
-	) {
-		if (configuration) {
-			this.configuration = configuration;
-			this.basePath = configuration.basePath || this.basePath;
-		}
-	}
-}
+    constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
+        if (configuration) {
+            this.configuration = configuration;
+            this.basePath = configuration.basePath || this.basePath;
+        }
+    }
+};
 
 /**
  *
@@ -67,8 +64,8 @@ export class BaseAPI {
  * @extends {Error}
  */
 export class RequiredError extends Error {
-	name: 'RequiredError' = 'RequiredError';
-	constructor(public field: string, msg?: string) {
-		super(msg);
-	}
+    name: "RequiredError" = "RequiredError";
+    constructor(public field: string, msg?: string) {
+        super(msg);
+    }
 }

+ 69 - 101
web/src/api/open-api/common.ts

@@ -5,166 +5,134 @@
  * Immich API
  *
  * The version of the OpenAPI document: 1.17.0
- *
+ * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * https://openapi-generator.tech
  * Do not edit the class manually.
  */
 
-import { Configuration } from './configuration';
-import { RequiredError, RequestArgs } from './base';
+
+import { Configuration } from "./configuration";
+import { RequiredError, RequestArgs } from "./base";
 import { AxiosInstance, AxiosResponse } from 'axios';
 
 /**
  *
  * @export
  */
-export const DUMMY_BASE_URL = 'https://example.com';
+export const DUMMY_BASE_URL = 'https://example.com'
 
 /**
  *
  * @throws {RequiredError}
  * @export
  */
-export const assertParamExists = function (
-	functionName: string,
-	paramName: string,
-	paramValue: unknown
-) {
-	if (paramValue === null || paramValue === undefined) {
-		throw new RequiredError(
-			paramName,
-			`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
-		);
-	}
-};
+export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
+    if (paramValue === null || paramValue === undefined) {
+        throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
+    }
+}
 
 /**
  *
  * @export
  */
-export const setApiKeyToObject = async function (
-	object: any,
-	keyParamName: string,
-	configuration?: Configuration
-) {
-	if (configuration && configuration.apiKey) {
-		const localVarApiKeyValue =
-			typeof configuration.apiKey === 'function'
-				? await configuration.apiKey(keyParamName)
-				: await configuration.apiKey;
-		object[keyParamName] = localVarApiKeyValue;
-	}
-};
+export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
+    if (configuration && configuration.apiKey) {
+        const localVarApiKeyValue = typeof configuration.apiKey === 'function'
+            ? await configuration.apiKey(keyParamName)
+            : await configuration.apiKey;
+        object[keyParamName] = localVarApiKeyValue;
+    }
+}
 
 /**
  *
  * @export
  */
 export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
-	if (configuration && (configuration.username || configuration.password)) {
-		object['auth'] = { username: configuration.username, password: configuration.password };
-	}
-};
+    if (configuration && (configuration.username || configuration.password)) {
+        object["auth"] = { username: configuration.username, password: configuration.password };
+    }
+}
 
 /**
  *
  * @export
  */
 export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
-	if (configuration && configuration.accessToken) {
-		const accessToken =
-			typeof configuration.accessToken === 'function'
-				? await configuration.accessToken()
-				: await configuration.accessToken;
-		object['Authorization'] = 'Bearer ' + accessToken;
-	}
-};
+    if (configuration && configuration.accessToken) {
+        const accessToken = typeof configuration.accessToken === 'function'
+            ? await configuration.accessToken()
+            : await configuration.accessToken;
+        object["Authorization"] = "Bearer " + accessToken;
+    }
+}
 
 /**
  *
  * @export
  */
-export const setOAuthToObject = async function (
-	object: any,
-	name: string,
-	scopes: string[],
-	configuration?: Configuration
-) {
-	if (configuration && configuration.accessToken) {
-		const localVarAccessTokenValue =
-			typeof configuration.accessToken === 'function'
-				? await configuration.accessToken(name, scopes)
-				: await configuration.accessToken;
-		object['Authorization'] = 'Bearer ' + localVarAccessTokenValue;
-	}
-};
+export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
+    if (configuration && configuration.accessToken) {
+        const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
+            ? await configuration.accessToken(name, scopes)
+            : await configuration.accessToken;
+        object["Authorization"] = "Bearer " + localVarAccessTokenValue;
+    }
+}
 
 /**
  *
  * @export
  */
 export const setSearchParams = function (url: URL, ...objects: any[]) {
-	const searchParams = new URLSearchParams(url.search);
-	for (const object of objects) {
-		for (const key in object) {
-			if (Array.isArray(object[key])) {
-				searchParams.delete(key);
-				for (const item of object[key]) {
-					searchParams.append(key, item);
-				}
-			} else {
-				searchParams.set(key, object[key]);
-			}
-		}
-	}
-	url.search = searchParams.toString();
-};
+    const searchParams = new URLSearchParams(url.search);
+    for (const object of objects) {
+        for (const key in object) {
+            if (Array.isArray(object[key])) {
+                searchParams.delete(key);
+                for (const item of object[key]) {
+                    searchParams.append(key, item);
+                }
+            } else {
+                searchParams.set(key, object[key]);
+            }
+        }
+    }
+    url.search = searchParams.toString();
+}
 
 /**
  *
  * @export
  */
-export const serializeDataIfNeeded = function (
-	value: any,
-	requestOptions: any,
-	configuration?: Configuration
-) {
-	const nonString = typeof value !== 'string';
-	const needsSerialization =
-		nonString && configuration && configuration.isJsonMime
-			? configuration.isJsonMime(requestOptions.headers['Content-Type'])
-			: nonString;
-	return needsSerialization ? JSON.stringify(value !== undefined ? value : {}) : value || '';
-};
+export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
+    const nonString = typeof value !== 'string';
+    const needsSerialization = nonString && configuration && configuration.isJsonMime
+        ? configuration.isJsonMime(requestOptions.headers['Content-Type'])
+        : nonString;
+    return needsSerialization
+        ? JSON.stringify(value !== undefined ? value : {})
+        : (value || "");
+}
 
 /**
  *
  * @export
  */
 export const toPathString = function (url: URL) {
-	return url.pathname + url.search + url.hash;
-};
+    return url.pathname + url.search + url.hash
+}
 
 /**
  *
  * @export
  */
-export const createRequestFunction = function (
-	axiosArgs: RequestArgs,
-	globalAxios: AxiosInstance,
-	BASE_PATH: string,
-	configuration?: Configuration
-) {
-	return <T = unknown, R = AxiosResponse<T>>(
-		axios: AxiosInstance = globalAxios,
-		basePath: string = BASE_PATH
-	) => {
-		const axiosRequestArgs = {
-			...axiosArgs.options,
-			url: (configuration?.basePath || basePath) + axiosArgs.url
-		};
-		return axios.request<T, R>(axiosRequestArgs);
-	};
-};
+export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
+    return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+        const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
+        return axios.request<T, R>(axiosRequestArgs);
+    };
+}

+ 81 - 101
web/src/api/open-api/configuration.ts

@@ -5,117 +5,97 @@
  * Immich API
  *
  * The version of the OpenAPI document: 1.17.0
- *
+ * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * https://openapi-generator.tech
  * Do not edit the class manually.
  */
 
+
 export interface ConfigurationParameters {
-	apiKey?:
-		| string
-		| Promise<string>
-		| ((name: string) => string)
-		| ((name: string) => Promise<string>);
-	username?: string;
-	password?: string;
-	accessToken?:
-		| string
-		| Promise<string>
-		| ((name?: string, scopes?: string[]) => string)
-		| ((name?: string, scopes?: string[]) => Promise<string>);
-	basePath?: string;
-	baseOptions?: any;
-	formDataCtor?: new () => any;
+    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
+    username?: string;
+    password?: string;
+    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
+    basePath?: string;
+    baseOptions?: any;
+    formDataCtor?: new () => any;
 }
 
 export class Configuration {
-	/**
-	 * parameter for apiKey security
-	 * @param name security name
-	 * @memberof Configuration
-	 */
-	apiKey?:
-		| string
-		| Promise<string>
-		| ((name: string) => string)
-		| ((name: string) => Promise<string>);
-	/**
-	 * parameter for basic security
-	 *
-	 * @type {string}
-	 * @memberof Configuration
-	 */
-	username?: string;
-	/**
-	 * parameter for basic security
-	 *
-	 * @type {string}
-	 * @memberof Configuration
-	 */
-	password?: string;
-	/**
-	 * parameter for oauth2 security
-	 * @param name security name
-	 * @param scopes oauth2 scope
-	 * @memberof Configuration
-	 */
-	accessToken?:
-		| string
-		| Promise<string>
-		| ((name?: string, scopes?: string[]) => string)
-		| ((name?: string, scopes?: string[]) => Promise<string>);
-	/**
-	 * override base path
-	 *
-	 * @type {string}
-	 * @memberof Configuration
-	 */
-	basePath?: string;
-	/**
-	 * base options for axios calls
-	 *
-	 * @type {any}
-	 * @memberof Configuration
-	 */
-	baseOptions?: any;
-	/**
-	 * The FormData constructor that will be used to create multipart form data
-	 * requests. You can inject this here so that execution environments that
-	 * do not support the FormData class can still run the generated client.
-	 *
-	 * @type {new () => FormData}
-	 */
-	formDataCtor?: new () => any;
+    /**
+     * parameter for apiKey security
+     * @param name security name
+     * @memberof Configuration
+     */
+    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
+    /**
+     * parameter for basic security
+     *
+     * @type {string}
+     * @memberof Configuration
+     */
+    username?: string;
+    /**
+     * parameter for basic security
+     *
+     * @type {string}
+     * @memberof Configuration
+     */
+    password?: string;
+    /**
+     * parameter for oauth2 security
+     * @param name security name
+     * @param scopes oauth2 scope
+     * @memberof Configuration
+     */
+    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
+    /**
+     * override base path
+     *
+     * @type {string}
+     * @memberof Configuration
+     */
+    basePath?: string;
+    /**
+     * base options for axios calls
+     *
+     * @type {any}
+     * @memberof Configuration
+     */
+    baseOptions?: any;
+    /**
+     * The FormData constructor that will be used to create multipart form data
+     * requests. You can inject this here so that execution environments that
+     * do not support the FormData class can still run the generated client.
+     *
+     * @type {new () => FormData}
+     */
+    formDataCtor?: new () => any;
 
-	constructor(param: ConfigurationParameters = {}) {
-		this.apiKey = param.apiKey;
-		this.username = param.username;
-		this.password = param.password;
-		this.accessToken = param.accessToken;
-		this.basePath = param.basePath;
-		this.baseOptions = param.baseOptions;
-		this.formDataCtor = param.formDataCtor;
-	}
+    constructor(param: ConfigurationParameters = {}) {
+        this.apiKey = param.apiKey;
+        this.username = param.username;
+        this.password = param.password;
+        this.accessToken = param.accessToken;
+        this.basePath = param.basePath;
+        this.baseOptions = param.baseOptions;
+        this.formDataCtor = param.formDataCtor;
+    }
 
-	/**
-	 * Check if the given MIME is a JSON MIME.
-	 * JSON MIME examples:
-	 *   application/json
-	 *   application/json; charset=UTF8
-	 *   APPLICATION/JSON
-	 *   application/vnd.company+json
-	 * @param mime - MIME (Multipurpose Internet Mail Extensions)
-	 * @return True if the given MIME is JSON, false otherwise.
-	 */
-	public isJsonMime(mime: string): boolean {
-		const jsonMime: RegExp = new RegExp(
-			'^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$',
-			'i'
-		);
-		return (
-			mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json')
-		);
-	}
+    /**
+     * Check if the given MIME is a JSON MIME.
+     * JSON MIME examples:
+     *   application/json
+     *   application/json; charset=UTF8
+     *   APPLICATION/JSON
+     *   application/vnd.company+json
+     * @param mime - MIME (Multipurpose Internet Mail Extensions)
+     * @return True if the given MIME is JSON, false otherwise.
+     */
+    public isJsonMime(mime: string): boolean {
+        const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
+        return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
+    }
 }

+ 5 - 3
web/src/api/open-api/index.ts

@@ -5,12 +5,14 @@
  * Immich API
  *
  * The version of the OpenAPI document: 1.17.0
- *
+ * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * https://openapi-generator.tech
  * Do not edit the class manually.
  */
 
-export * from './api';
-export * from './configuration';
+
+export * from "./api";
+export * from "./configuration";
+

+ 43 - 1
web/src/lib/components/album-page/asset-selection.svelte

@@ -9,6 +9,8 @@
 	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();
 
@@ -19,7 +21,41 @@
 	let existingGroup: Set<number> = new Set();
 	let groupWithAssetsInAlbum: Record<number, Set<string>> = {};
 
-	onMount(() => scanForExistingSelectedGroup());
+	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);
@@ -146,6 +182,12 @@
 		</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}

+ 13 - 0
web/src/lib/stores/album-upload-asset.ts

@@ -0,0 +1,13 @@
+import { writable } from 'svelte/store';
+
+function createAlbumUploadStore() {
+	const albumUploadAsset = writable<Array<string>>([]);
+	const albumUploadAssetCount = writable<number>(9999);
+
+	return {
+		asset: albumUploadAsset,
+		count: albumUploadAssetCount
+	};
+}
+
+export const albumUploadAssetStore = createAlbumUploadStore();

+ 71 - 3
web/src/lib/utils/file-uploader.ts

@@ -3,9 +3,58 @@ import * as exifr from 'exifr';
 import { serverEndpoint } from '../constants';
 import { uploadAssetsStore } from '$lib/stores/upload';
 import type { UploadAsset } from '../models/upload-asset';
-import { api } from '@api';
+import { api, AssetFileUploadResponseDto } from '@api';
+import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
+
+/**
+ * Determine if the upload is for album or for the user general backup
+ * @variant GENERAL - Upload assets to the server for general backup
+ * @variant ALBUM - Upload assets to the server for backup and add to the album
+ */
+export enum UploadType {
+	/**
+	 * Upload assets to the server
+	 */
+	GENERAL = 'GENERAL',
+
+	/**
+	 * Upload assets to the server and add to album
+	 */
+	ALBUM = 'ALBUM'
+}
+
+export const openFileUploadDialog = (uploadType: UploadType) => {
+	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);
 
-export async function fileUploader(asset: File) {
+			const acceptedFile = files.filter(
+				(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
+			);
+
+			if (uploadType === UploadType.ALBUM) {
+				albumUploadAssetStore.asset.set([]);
+				albumUploadAssetStore.count.set(acceptedFile.length);
+			}
+
+			for (const asset of acceptedFile) {
+				await fileUploader(asset, uploadType);
+			}
+		};
+
+		fileSelector.click();
+	} catch (e) {
+		console.log('Error seelcting file', e);
+	}
+};
+
+async function fileUploader(asset: File, uploadType: UploadType) {
 	const assetType = asset.type.split('/')[0].toUpperCase();
 	const temp = asset.name.split('.');
 	const fileExtension = temp[temp.length - 1];
@@ -61,6 +110,11 @@ export async function fileUploader(asset: File) {
 
 		if (status === 200) {
 			if (data.isExist) {
+				if (uploadType === UploadType.ALBUM && data.id) {
+					albumUploadAssetStore.asset.update((a) => {
+						return [...a, data.id!];
+					});
+				}
 				return;
 			}
 		}
@@ -78,12 +132,26 @@ export async function fileUploader(asset: File) {
 			uploadAssetsStore.addNewUploadAsset(newUploadAsset);
 		};
 
-		request.upload.onload = () => {
+		request.upload.onload = (e) => {
 			setTimeout(() => {
 				uploadAssetsStore.removeUploadAsset(deviceAssetId);
 			}, 1000);
 		};
 
+		request.onreadystatechange = () => {
+			try {
+				if (request.readyState === 4 && uploadType === UploadType.ALBUM) {
+					const res: AssetFileUploadResponseDto = JSON.parse(request.response);
+
+					albumUploadAssetStore.asset.update((assets) => {
+						return [...assets, res.id];
+					});
+				}
+			} catch (e) {
+				console.error('ERROR parsing data JSON in upload onreadystatechange');
+			}
+		};
+
 		// listen for `error` event
 		request.upload.onerror = () => {
 			uploadAssetsStore.removeUploadAsset(deviceAssetId);

+ 2 - 28
web/src/routes/photos/index.svelte

@@ -32,7 +32,7 @@
 	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 { fileUploader } from '$lib/utils/file-uploader';
+	import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
 	import { api, AssetResponseDto, UserResponseDto } from '@api';
 	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
 
@@ -64,32 +64,6 @@
 		pushState(selectedAsset.id);
 	};
 
-	const uploadClickedHandler = async () => {
-		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);
-				}
-			};
-
-			fileSelector.click();
-		} catch (e) {
-			console.log('Error seelcting file', e);
-		}
-	};
-
 	const navigateAssetForward = () => {
 		try {
 			if (currentViewAssetIndex < $flattenAssetGroupByDate.length - 1) {
@@ -131,7 +105,7 @@
 </svelte:head>
 
 <section>
-	<NavigationBar {user} on:uploadClicked={uploadClickedHandler} />
+	<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
 </section>
 
 <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">

Деякі файли не було показано, через те що забагато файлів було змінено