1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01: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:
Alex 2022-07-26 20:53:25 -05:00 committed by GitHub
parent 2336a6159c
commit 03457f5d32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 3823 additions and 4408 deletions

View file

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

View file

@ -14,25 +14,41 @@ class CheckDuplicateAssetResponseDto {
/// Returns a new [CheckDuplicateAssetResponseDto] instance. /// Returns a new [CheckDuplicateAssetResponseDto] instance.
CheckDuplicateAssetResponseDto({ CheckDuplicateAssetResponseDto({
required this.isExist, required this.isExist,
this.id,
}); });
bool isExist; 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 @override
bool operator ==(Object other) => identical(this, other) || other is CheckDuplicateAssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is CheckDuplicateAssetResponseDto &&
other.isExist == isExist; other.isExist == isExist &&
other.id == id;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(isExist.hashCode); (isExist.hashCode) +
(id == null ? 0 : id!.hashCode);
@override @override
String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist]'; String toString() => 'CheckDuplicateAssetResponseDto[isExist=$isExist, id=$id]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'isExist'] = isExist; _json[r'isExist'] = isExist;
if (id != null) {
_json[r'id'] = id;
} else {
_json[r'id'] = null;
}
return _json; return _json;
} }
@ -56,6 +72,7 @@ class CheckDuplicateAssetResponseDto {
return CheckDuplicateAssetResponseDto( return CheckDuplicateAssetResponseDto(
isExist: mapValueOfType<bool>(json, r'isExist')!, isExist: mapValueOfType<bool>(json, r'isExist')!,
id: mapValueOfType<String>(json, r'id'),
); );
} }
return null; return null;

View file

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

View file

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

View file

@ -1,6 +1,8 @@
export class CheckDuplicateAssetResponseDto { export class CheckDuplicateAssetResponseDto {
constructor(isExist: boolean) { constructor(isExist: boolean, id?: string) {
this.isExist = isExist; this.isExist = isExist;
this.id = id;
} }
isExist: boolean; 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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import { AssetResponseDto } from '@api'; import { AssetResponseDto } from '@api';
import AlbumAppBar from './album-app-bar.svelte'; 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(); const dispatch = createEventDispatcher();
@ -19,7 +21,41 @@
let existingGroup: Set<number> = new Set(); let existingGroup: Set<number> = new Set();
let groupWithAssetsInAlbum: Record<number, Set<string>> = {}; 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 selectAssetHandler = (assetId: string, groupIndex: number) => {
const tempSelectedAsset = new Set(selectedAsset); const tempSelectedAsset = new Set(selectedAsset);
@ -146,6 +182,12 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="trailing"> <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 <button
disabled={selectedAsset.size === 0} disabled={selectedAsset.size === 0}
on:click={addSelectedAssets} on:click={addSelectedAssets}

View 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();

View file

@ -3,9 +3,58 @@ import * as exifr from 'exifr';
import { serverEndpoint } from '../constants'; import { serverEndpoint } from '../constants';
import { uploadAssetsStore } from '$lib/stores/upload'; import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset'; 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 assetType = asset.type.split('/')[0].toUpperCase();
const temp = asset.name.split('.'); const temp = asset.name.split('.');
const fileExtension = temp[temp.length - 1]; const fileExtension = temp[temp.length - 1];
@ -61,6 +110,11 @@ export async function fileUploader(asset: File) {
if (status === 200) { if (status === 200) {
if (data.isExist) { if (data.isExist) {
if (uploadType === UploadType.ALBUM && data.id) {
albumUploadAssetStore.asset.update((a) => {
return [...a, data.id!];
});
}
return; return;
} }
} }
@ -78,12 +132,26 @@ export async function fileUploader(asset: File) {
uploadAssetsStore.addNewUploadAsset(newUploadAsset); uploadAssetsStore.addNewUploadAsset(newUploadAsset);
}; };
request.upload.onload = () => { request.upload.onload = (e) => {
setTimeout(() => { setTimeout(() => {
uploadAssetsStore.removeUploadAsset(deviceAssetId); uploadAssetsStore.removeUploadAsset(deviceAssetId);
}, 1000); }, 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 // listen for `error` event
request.upload.onerror = () => { request.upload.onerror = () => {
uploadAssetsStore.removeUploadAsset(deviceAssetId); uploadAssetsStore.removeUploadAsset(deviceAssetId);

View file

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