mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
merge main
This commit is contained in:
Generated
+98
-28
@@ -1182,22 +1182,6 @@ export interface CheckExistingAssetsResponseDto {
|
||||
*/
|
||||
'existingIds': Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const CitiesFile = {
|
||||
Cities15000: 'cities15000',
|
||||
Cities5000: 'cities5000',
|
||||
Cities1000: 'cities1000',
|
||||
Cities500: 'cities500'
|
||||
} as const;
|
||||
|
||||
export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -3850,12 +3834,6 @@ export interface SystemConfigPasswordLoginDto {
|
||||
* @interface SystemConfigReverseGeocodingDto
|
||||
*/
|
||||
export interface SystemConfigReverseGeocodingDto {
|
||||
/**
|
||||
*
|
||||
* @type {CitiesFile}
|
||||
* @memberof SystemConfigReverseGeocodingDto
|
||||
*/
|
||||
'citiesFileOverride': CitiesFile;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -3863,8 +3841,6 @@ export interface SystemConfigReverseGeocodingDto {
|
||||
*/
|
||||
'enabled': boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -6844,6 +6820,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
* @param {string} deviceId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'deviceId' is not null or undefined
|
||||
assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId)
|
||||
const localVarPath = `/asset/device/{deviceId}`
|
||||
.replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -7513,9 +7531,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*
|
||||
* @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
|
||||
* @param {string} deviceId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @deprecated
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
@@ -8347,6 +8367,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
* @param {string} deviceId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Get a single asset\'s information
|
||||
* @param {string} id
|
||||
@@ -8494,9 +8524,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*
|
||||
* @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
|
||||
* @param {string} deviceId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @deprecated
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
|
||||
@@ -8722,6 +8754,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
||||
return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
* @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise<Array<string>> {
|
||||
return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Get a single asset\'s information
|
||||
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
|
||||
@@ -8828,9 +8869,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*
|
||||
* @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
|
||||
* @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @deprecated
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise<Array<string>> {
|
||||
@@ -9066,6 +9109,20 @@ export interface AssetApiGetAllAssetsRequest {
|
||||
readonly ifNoneMatch?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getAllUserAssetsByDeviceId operation in AssetApi.
|
||||
* @export
|
||||
* @interface AssetApiGetAllUserAssetsByDeviceIdRequest
|
||||
*/
|
||||
export interface AssetApiGetAllUserAssetsByDeviceIdRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetApiGetAllUserAssetsByDeviceId
|
||||
*/
|
||||
readonly deviceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getAssetById operation in AssetApi.
|
||||
* @export
|
||||
@@ -10010,6 +10067,17 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
* @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single asset\'s information
|
||||
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
|
||||
@@ -10140,9 +10208,11 @@ export class AssetApi extends BaseAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*
|
||||
* @summary Use /asset/device/:deviceId instead - Remove in 1.92 release
|
||||
* @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @deprecated
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
|
||||
const restoreUser = async () => {
|
||||
const restoredUser = await api.userApi.restoreUser({ id: user.id });
|
||||
if (restoredUser.data.deletedAt == null) dispatch('user-restore-success');
|
||||
else dispatch('user-restore-fail');
|
||||
if (restoredUser.data.deletedAt == null) {
|
||||
dispatch('user-restore-success');
|
||||
} else {
|
||||
dispatch('user-restore-fail');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
return '0'.repeat(zeroLength);
|
||||
};
|
||||
|
||||
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0);
|
||||
const TiB = 1024 ** 4;
|
||||
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
@@ -16,8 +16,18 @@
|
||||
let savedConfig: SystemConfigJobDto;
|
||||
let defaultConfig: SystemConfigJobDto;
|
||||
|
||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
|
||||
const jobNames = [
|
||||
JobName.ThumbnailGeneration,
|
||||
JobName.MetadataExtraction,
|
||||
JobName.Library,
|
||||
JobName.Sidecar,
|
||||
JobName.ObjectTagging,
|
||||
JobName.ClipEncoding,
|
||||
JobName.RecognizeFaces,
|
||||
JobName.VideoConversion,
|
||||
JobName.StorageTemplateMigration,
|
||||
JobName.Migration,
|
||||
];
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, CitiesFile, SystemConfigDto } from '@api';
|
||||
import { api, SystemConfigDto } from '@api';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingAccordion from '../setting-accordion.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import SettingSelect from '../setting-select.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
@@ -39,7 +38,6 @@
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: config.reverseGeocoding.enabled,
|
||||
citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -131,24 +129,6 @@
|
||||
subtitle="Enable reverse geocoding"
|
||||
bind:checked={config.reverseGeocoding.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label="Precision"
|
||||
desc="Set reverse geocoding precision"
|
||||
name="reverse-geocoding-precision"
|
||||
bind:value={config.reverseGeocoding.citiesFileOverride}
|
||||
options={[
|
||||
{ value: CitiesFile.Cities500, text: 'Cities with more than 500 people' },
|
||||
{ value: CitiesFile.Cities1000, text: 'Cities with more than 1000 people' },
|
||||
{ value: CitiesFile.Cities5000, text: 'Cities with more than 5000 people' },
|
||||
{ value: CitiesFile.Cities15000, text: 'Cities with more than 15000 people' },
|
||||
]}
|
||||
disabled={disabled || !config.reverseGeocoding.enabled}
|
||||
isEdited={config.reverseGeocoding.citiesFileOverride !==
|
||||
savedConfig.reverseGeocoding.citiesFileOverride}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
|
||||
@@ -44,14 +44,10 @@
|
||||
>
|
||||
<span class="flex gap-1 text-sm">
|
||||
{#if variant === 'simple'}
|
||||
<span
|
||||
>{#if album.shared}Shared{/if}
|
||||
</span>
|
||||
<span>{album.shared ? 'Shared' : ''}</span>
|
||||
{:else}
|
||||
<span>{album.assetCount} items</span>
|
||||
<span
|
||||
>{#if album.shared} · Shared{/if}
|
||||
</span>
|
||||
<span>{album.shared ? ' · Shared' : ''} </span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
export let circle = false;
|
||||
export let hidden = false;
|
||||
export let border = false;
|
||||
export let preload = true;
|
||||
let complete = false;
|
||||
|
||||
export let eyeColor: 'black' | 'white' = 'white';
|
||||
</script>
|
||||
|
||||
<img
|
||||
loading={preload ? 'eager' : 'lazy'}
|
||||
style:width={widthStyle}
|
||||
style:height={heightStyle}
|
||||
style:filter={hidden ? 'grayscale(50%)' : 'none'}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
export let option: Sort;
|
||||
|
||||
const handleSort = () => {
|
||||
if (albumViewSettings === option.sortTitle) {
|
||||
if (albumViewSettings === option.title) {
|
||||
option.sortDesc = !option.sortDesc;
|
||||
} else {
|
||||
albumViewSettings = option.sortTitle;
|
||||
albumViewSettings = option.title;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -18,12 +18,12 @@
|
||||
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
||||
on:click={() => handleSort()}
|
||||
>
|
||||
{#if albumViewSettings === option.sortTitle}
|
||||
{#if albumViewSettings === option.title}
|
||||
{#if option.sortDesc}
|
||||
↓
|
||||
{:else}
|
||||
↑
|
||||
{/if}
|
||||
{/if}{option.table}</button
|
||||
{/if}{option.title}</button
|
||||
></th
|
||||
>
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import { mdiCallMerge, mdiClose, mdiMagnify, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
let people: PersonResponseDto[] = [];
|
||||
let peopleCopy: PersonResponseDto[] = [];
|
||||
let selectedPeople: PersonResponseDto[] = [];
|
||||
let screenHeight: number;
|
||||
let isShowConfirmation = false;
|
||||
let name = '';
|
||||
let searchWord: string;
|
||||
let isSearchingPeople = false;
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
$: hasSelection = selectedPeople.length > 0;
|
||||
@@ -31,12 +37,49 @@
|
||||
onMount(async () => {
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: false });
|
||||
people = data.people;
|
||||
peopleCopy = cloneDeep(people);
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
dispatch('go-back');
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
name = '';
|
||||
people = peopleCopy;
|
||||
};
|
||||
|
||||
const searchPeople = async (force: boolean) => {
|
||||
if (name === '') {
|
||||
people = peopleCopy;
|
||||
return;
|
||||
}
|
||||
if (!force) {
|
||||
if (people.length < 20 && name.startsWith(searchWord)) {
|
||||
people = peopleCopy
|
||||
.filter((person: PersonResponseDto) => {
|
||||
const nameParts = person.name.split(' ');
|
||||
return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase()));
|
||||
})
|
||||
.slice(0, 10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
|
||||
try {
|
||||
const { data } = await api.searchApi.searchPerson({ name });
|
||||
people = data;
|
||||
searchWord = name;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't search people");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
isSearchingPeople = false;
|
||||
};
|
||||
|
||||
const handleSwapPeople = () => {
|
||||
[person, selectedPeople[0]] = [selectedPeople[0], person];
|
||||
goto(`${AppRoute.PEOPLE}/${person.id}?action=merge`);
|
||||
@@ -136,9 +179,39 @@
|
||||
<FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
|
||||
style:max-height={screenHeight - 200 - 200 + 'px'}
|
||||
class="flex w-40 sm:w-48 md:w-96 h-14 rounded-lg bg-gray-100 p-2 dark:bg-gray-700 mb-8 gap-2 place-items-center"
|
||||
>
|
||||
<button on:click={() => searchPeople(true)}>
|
||||
<div class="w-fit">
|
||||
<Icon path={mdiMagnify} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
placeholder="Search names"
|
||||
bind:value={name}
|
||||
on:input={() => searchPeople(false)}
|
||||
/>
|
||||
{#if name}
|
||||
<button on:click={resetSearch}>
|
||||
<Icon path={mdiClose} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if isSearchingPeople}
|
||||
<div class="flex place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 pt-8 px-8 pb-10 dark:bg-immich-dark-gray"
|
||||
style:max-height={screenHeight - 250 - 250 + 'px'}
|
||||
>
|
||||
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
|
||||
{#each unselectedPeople as person (person.id)}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
export let preload = false;
|
||||
|
||||
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face';
|
||||
let dispatch = createEventDispatcher<{
|
||||
@@ -48,6 +49,7 @@
|
||||
<div class="h-48 w-48 rounded-xl brightness-95 filter">
|
||||
<ImageThumbnail
|
||||
shadow
|
||||
{preload}
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
|
||||
@@ -275,6 +275,8 @@
|
||||
|
||||
<style>
|
||||
.main-view {
|
||||
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.3), 0 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
box-shadow:
|
||||
0 4px 4px 0 rgba(0, 0, 0, 0.3),
|
||||
0 8px 12px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
|
||||
<style>
|
||||
.memory-card {
|
||||
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
|
||||
box-shadow:
|
||||
rgba(60, 64, 67, 0.3) 0px 1px 2px 0px,
|
||||
rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<svelte:fragment slot="title">
|
||||
<span class="flex place-items-center gap-2">
|
||||
<p class="font-medium">
|
||||
Add to {#if shared}Shared {/if} Album
|
||||
Add to {shared ? 'Shared ' : ''}Album
|
||||
</p>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
@@ -88,7 +88,7 @@
|
||||
<Icon path={mdiPlus} size="30" />
|
||||
</div>
|
||||
<p class="">
|
||||
New {#if shared}Shared {/if}Album {#if search.length > 0}<b>{search}</b>{/if}
|
||||
New {shared ? 'Shared ' : ''}Album {#if search.length > 0}<b>{search}</b>{/if}
|
||||
</p>
|
||||
</button>
|
||||
{#if filteredAlbums.length > 0}
|
||||
@@ -101,7 +101,8 @@
|
||||
|
||||
{#if !shared}
|
||||
<p class="px-5 py-3 text-xs">
|
||||
{#if search.length === 0}ALL {/if}ALBUMS
|
||||
{#if search.length === 0}ALL
|
||||
{/if}ALBUMS
|
||||
</p>
|
||||
{/if}
|
||||
{#each filteredAlbums as album (album.id)}
|
||||
|
||||
@@ -111,10 +111,20 @@
|
||||
</script>
|
||||
|
||||
{#await style then style}
|
||||
<MapLibre {style} class="h-full" {center} {zoom} attributionControl={false} diffStyleUpdates={true} let:map bind:map>
|
||||
<MapLibre
|
||||
{style}
|
||||
class="h-full"
|
||||
{center}
|
||||
{zoom}
|
||||
attributionControl={false}
|
||||
diffStyleUpdates={true}
|
||||
let:map
|
||||
on:load={(event) => event.detail.setMaxZoom(14)}
|
||||
bind:map
|
||||
>
|
||||
<NavigationControl position="top-left" showCompass={!simplified} />
|
||||
{#if !simplified}
|
||||
<GeolocateControl position="top-left" fitBoundsOptions={{ maxZoom: 12 }} />
|
||||
<GeolocateControl position="top-left" />
|
||||
<FullscreenControl position="top-left" />
|
||||
<ScaleControl />
|
||||
<AttributionControl compact={false} />
|
||||
@@ -134,7 +144,7 @@
|
||||
}),
|
||||
}}
|
||||
id="geojson"
|
||||
cluster={{ maxZoom: 14, radius: 500 }}
|
||||
cluster={{ radius: 500 }}
|
||||
>
|
||||
<MarkerLayer
|
||||
applyToClusters
|
||||
|
||||
@@ -66,13 +66,17 @@
|
||||
<div class="h-[15px] rounded-md bg-immich-warning transition-all" style="width: 100%" />
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
||||
Skipped
|
||||
{#if uploadAsset.message} ({uploadAsset.message}){/if}
|
||||
{#if uploadAsset.message}
|
||||
({uploadAsset.message})
|
||||
{/if}
|
||||
</p>
|
||||
{:else if uploadAsset.state === UploadState.DONE}
|
||||
<div class="h-[15px] rounded-md bg-immich-success transition-all" style="width: 100%" />
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
||||
Uploaded
|
||||
{#if uploadAsset.message} ({uploadAsset.message}){/if}
|
||||
{#if uploadAsset.message}
|
||||
({uploadAsset.message})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
|
||||
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
|
||||
|
||||
export let link: SharedLinkResponseDto;
|
||||
|
||||
let expirationCountdown: luxon.DurationObjectUnits;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const getAssetInfo = async (): Promise<AssetResponseDto> => {
|
||||
const getThumbnail = async (): Promise<AssetResponseDto> => {
|
||||
let assetId = '';
|
||||
|
||||
if (link.album?.albumThumbnailAssetId) {
|
||||
@@ -60,18 +61,28 @@
|
||||
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
|
||||
>
|
||||
<div>
|
||||
{#await getAssetInfo()}
|
||||
<LoadingSpinner />
|
||||
{:then asset}
|
||||
{#if link?.album?.albumThumbnailAssetId || link.assets.length > 0}
|
||||
{#await getThumbnail()}
|
||||
<LoadingSpinner />
|
||||
{:then asset}
|
||||
<img
|
||||
id={asset.id}
|
||||
src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
|
||||
alt={asset.id}
|
||||
class="h-[100px] w-[100px] rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<img
|
||||
id={asset.id}
|
||||
src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
|
||||
alt={asset.id}
|
||||
src={noThumbnailUrl}
|
||||
alt={'Album without assets'}
|
||||
class="h-[100px] w-[100px] rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between">
|
||||
|
||||
@@ -73,7 +73,10 @@ export class AssetStore {
|
||||
assets: AssetResponseDto[] = [];
|
||||
albumAssets: Set<string> = new Set();
|
||||
|
||||
constructor(options: AssetStoreOptions, private albumId?: string) {
|
||||
constructor(
|
||||
options: AssetStoreOptions,
|
||||
private albumId?: string,
|
||||
) {
|
||||
this.options = { ...options, size: TimeBucketSize.Month };
|
||||
this.store$.set(this);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,26 @@
|
||||
export function getThumbnailSize(assetCount: number, viewWidth: number): number {
|
||||
if (assetCount < 6) {
|
||||
return Math.min(320, Math.floor(viewWidth / assetCount - assetCount));
|
||||
} else {
|
||||
if (viewWidth > 600) return viewWidth / 7 - 7;
|
||||
else if (viewWidth > 400) return viewWidth / 4 - 6;
|
||||
else if (viewWidth > 300) return viewWidth / 2 - 6;
|
||||
else if (viewWidth > 200) return viewWidth / 2 - 6;
|
||||
else if (viewWidth > 100) return viewWidth / 1 - 6;
|
||||
}
|
||||
|
||||
if (viewWidth > 600) {
|
||||
return viewWidth / 7 - 7;
|
||||
}
|
||||
|
||||
if (viewWidth > 400) {
|
||||
return viewWidth / 4 - 6;
|
||||
}
|
||||
|
||||
if (viewWidth > 300) {
|
||||
return viewWidth / 2 - 6;
|
||||
}
|
||||
|
||||
if (viewWidth > 200) {
|
||||
return viewWidth / 2 - 6;
|
||||
}
|
||||
|
||||
if (viewWidth > 100) {
|
||||
return viewWidth / 1 - 6;
|
||||
}
|
||||
|
||||
return 300;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
// table is the text printed in the table and sortTitle is the text printed in the dropDow menu
|
||||
|
||||
export interface Sort {
|
||||
table: string;
|
||||
sortTitle: string;
|
||||
title: string;
|
||||
sortDesc: boolean;
|
||||
widthClass: string;
|
||||
sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
|
||||
@@ -54,46 +51,75 @@
|
||||
|
||||
let sortByOptions: Record<string, Sort> = {
|
||||
albumTitle: {
|
||||
table: 'Album title',
|
||||
sortTitle: 'Album title',
|
||||
title: 'Album title',
|
||||
sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction
|
||||
widthClass: 'w-8/12 text-left sm:w-4/12 md:w-4/12 md:w-4/12 2xl:w-6/12',
|
||||
widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
|
||||
},
|
||||
},
|
||||
numberOfAssets: {
|
||||
table: 'Assets',
|
||||
sortTitle: 'Number of assets',
|
||||
title: 'Number of assets',
|
||||
sortDesc: $albumViewSettings.sortDesc,
|
||||
widthClass: 'w-4/12 text-center sm:w-2/12 2xl:w-1/12',
|
||||
widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
|
||||
},
|
||||
},
|
||||
lastModified: {
|
||||
table: 'Updated date',
|
||||
sortTitle: 'Last modified',
|
||||
title: 'Last modified',
|
||||
sortDesc: $albumViewSettings.sortDesc,
|
||||
widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
|
||||
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']);
|
||||
},
|
||||
},
|
||||
mostRecent: {
|
||||
table: 'Created date',
|
||||
sortTitle: 'Most recent photo',
|
||||
created: {
|
||||
title: 'Created date',
|
||||
sortDesc: $albumViewSettings.sortDesc,
|
||||
widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
|
||||
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']);
|
||||
},
|
||||
},
|
||||
mostRecent: {
|
||||
title: 'Most recent photo',
|
||||
sortDesc: $albumViewSettings.sortDesc,
|
||||
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(
|
||||
albums,
|
||||
[
|
||||
(album) =>
|
||||
album.lastModifiedAssetTimestamp ? new Date(album.lastModifiedAssetTimestamp) : new Date(album.updatedAt),
|
||||
],
|
||||
[(album) => (album.endDate ? new Date(album.endDate) : '')],
|
||||
[reverse ? 'desc' : 'asc'],
|
||||
);
|
||||
).sort((a, b) => {
|
||||
if (a.endDate === undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (b.endDate === undefined) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
mostOld: {
|
||||
title: 'Oldest photo',
|
||||
sortDesc: $albumViewSettings.sortDesc,
|
||||
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
|
||||
sortFn: (reverse, albums) => {
|
||||
return orderBy(
|
||||
albums,
|
||||
[(album) => (album.startDate ? new Date(album.startDate) : null)],
|
||||
[reverse ? 'desc' : 'asc'],
|
||||
).sort((a, b) => {
|
||||
if (a.startDate === undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (b.startDate === undefined) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -144,16 +170,25 @@
|
||||
};
|
||||
|
||||
$: {
|
||||
const { sortBy } = $albumViewSettings;
|
||||
for (const key in sortByOptions) {
|
||||
if (sortByOptions[key].sortTitle === sortBy) {
|
||||
if (sortByOptions[key].title === $albumViewSettings.sortBy) {
|
||||
$albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
|
||||
$albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
|
||||
$albumViewSettings.sortBy = sortByOptions[key].title;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const test = (searched: string): Sort => {
|
||||
for (const key in sortByOptions) {
|
||||
if (sortByOptions[key].title === searched) {
|
||||
return sortByOptions[key];
|
||||
}
|
||||
}
|
||||
return sortByOptions[0];
|
||||
};
|
||||
|
||||
const handleCreateAlbum = async () => {
|
||||
const newAlbum = await createAlbum();
|
||||
if (newAlbum) {
|
||||
@@ -220,19 +255,20 @@
|
||||
|
||||
<Dropdown
|
||||
options={Object.values(sortByOptions)}
|
||||
selectedOption={test($albumViewSettings.sortBy)}
|
||||
render={(option) => {
|
||||
return {
|
||||
title: option.sortTitle,
|
||||
title: option.title,
|
||||
icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
|
||||
};
|
||||
}}
|
||||
on:select={(event) => {
|
||||
for (const key in sortByOptions) {
|
||||
if (sortByOptions[key].sortTitle === event.detail.sortTitle) {
|
||||
if (sortByOptions[key].title === event.detail.title) {
|
||||
sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
|
||||
$albumViewSettings.sortBy = sortByOptions[key].title;
|
||||
}
|
||||
}
|
||||
$albumViewSettings.sortBy = event.detail.sortTitle;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -251,7 +287,7 @@
|
||||
{#if $albums.length !== 0}
|
||||
<!-- Album Card -->
|
||||
{#if $albumViewSettings.view === AlbumViewMode.Cover}
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]">
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
|
||||
{#each $albums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard
|
||||
@@ -271,7 +307,7 @@
|
||||
{#each Object.keys(sortByOptions) as key (key)}
|
||||
<TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
|
||||
{/each}
|
||||
<th class="hidden w-2/12 text-center text-sm font-medium lg:block 2xl:w-1/12">Action</th>
|
||||
<th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
@@ -284,18 +320,34 @@
|
||||
on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
|
||||
tabindex="0"
|
||||
>
|
||||
<td class="text-md w-8/12 text-ellipsis text-left sm:w-4/12 md:w-4/12 2xl:w-6/12">{album.albumName}</td>
|
||||
<td class="text-md w-4/12 text-ellipsis text-center sm:w-2/12 md:w-2/12 2xl:w-1/12">
|
||||
{album.assetCount}
|
||||
{album.assetCount == 1 ? `item` : `items`}
|
||||
</td>
|
||||
<td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
|
||||
>{dateLocaleString(album.updatedAt)}</td
|
||||
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]"
|
||||
>{album.albumName}</td
|
||||
>
|
||||
<td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
|
||||
<td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
|
||||
{album.assetCount}
|
||||
{album.assetCount > 1 ? `items` : `item`}
|
||||
</td>
|
||||
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
|
||||
>{dateLocaleString(album.updatedAt)}
|
||||
</td>
|
||||
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
|
||||
>{dateLocaleString(album.createdAt)}</td
|
||||
>
|
||||
<td class="text-md hidden w-2/12 text-ellipsis text-center lg:block 2xl:w-1/12">
|
||||
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
|
||||
{#if album.endDate}
|
||||
{dateLocaleString(album.endDate)}
|
||||
{:else}
|
||||
❌
|
||||
{/if}</td
|
||||
>
|
||||
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"
|
||||
>{#if album.startDate}
|
||||
{dateLocaleString(album.startDate)}
|
||||
{:else}
|
||||
❌
|
||||
{/if}</td
|
||||
>
|
||||
<td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]">
|
||||
<button
|
||||
on:click|stopPropagation={() => handleEdit(album)}
|
||||
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
|
||||
|
||||
@@ -372,10 +372,11 @@
|
||||
{#if countVisiblePeople > 0}
|
||||
<div class="pl-4">
|
||||
<div class="flex flex-row flex-wrap gap-1">
|
||||
{#each people as person (person.id)}
|
||||
{#each people as person, idx (person.id)}
|
||||
{#if !person.isHidden}
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={idx < 20}
|
||||
on:change-name={() => handleChangeName(person)}
|
||||
on:set-birth-date={() => handleSetBirthDate(person)}
|
||||
on:merge-faces={() => handleMergeFaces(person)}
|
||||
@@ -444,7 +445,7 @@
|
||||
bind:showLoadingSpinner
|
||||
bind:toggleVisibility
|
||||
>
|
||||
{#each people as person (person.id)}
|
||||
{#each people as person, idx (person.id)}
|
||||
<button
|
||||
class="relative h-36 w-36 md:h-48 md:w-48"
|
||||
on:click={() => (person.isHidden = !person.isHidden)}
|
||||
@@ -452,6 +453,7 @@
|
||||
on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={idx < 20}
|
||||
bind:hidden={person.isHidden}
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||
import ChangeDate from '$lib/components/photos-page/actions/change-location-action.svelte';
|
||||
import ChangeLocation from '$lib/components/photos-page/actions/change-datetion-action.svelte';
|
||||
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
||||
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
@@ -137,7 +137,7 @@
|
||||
{#if albums.length}
|
||||
<section>
|
||||
<div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">ALBUMS</div>
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]">
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
|
||||
{#each albums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard {album} user={data.user} isSharingView={false} showItemCount={false} showContextMenu={false} />
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<div>
|
||||
<!-- Share Album List -->
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]">
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
|
||||
{#each data.sharedAlbums as album (album.id)}
|
||||
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
|
||||
<AlbumCard {album} user={data.user} isSharingView showContextMenu={false} />
|
||||
|
||||
@@ -181,8 +181,8 @@
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="w-4/12 text-ellipsis break-all px-2 text-sm">{user.email}</td>
|
||||
@@ -247,8 +247,8 @@
|
||||
isDeleted(user)
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis break-words px-2 text-sm">{user.name}</td>
|
||||
|
||||
Reference in New Issue
Block a user