mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	[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
This commit is contained in:
		| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -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,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"; | ||||
|  | ||||
|   | ||||
| @@ -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
									
								
								web/src/lib/stores/album-upload-asset.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/lib/stores/album-upload-asset.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
| @@ -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'; | ||||
|  | ||||
| export async function fileUploader(asset: File) { | ||||
| /** | ||||
|  * 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); | ||||
|  | ||||
| 			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); | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user