mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +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:
parent
2336a6159c
commit
03457f5d32
15 changed files with 3802 additions and 4405 deletions
Binary file not shown.
Binary file not shown.
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue