merge main
This commit is contained in:
@@ -51,6 +51,7 @@ module.exports = {
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'svelte/button-has-type': 'error',
|
||||
// TODO: set recommended-type-checked and remove these rules
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
@@ -20,9 +20,9 @@
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
try {
|
||||
const { deletedAt } = await deleteUser({
|
||||
const { deletedAt } = await deleteUserAdmin({
|
||||
id: user.id,
|
||||
deleteUserDto: { force: forceDelete },
|
||||
userAdminDeleteDto: { force: forceDelete },
|
||||
});
|
||||
|
||||
if (deletedAt == undefined) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
{disabled}
|
||||
class="flex h-full w-full flex-col place-content-center place-items-center gap-2 px-8 py-2 text-xs text-gray-600 transition-colors dark:text-gray-200 {colorClasses[
|
||||
color
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
const handleRestoreUser = async () => {
|
||||
try {
|
||||
const { deletedAt } = await restoreUser({ id: user.id });
|
||||
const { deletedAt } = await restoreUserAdmin({ id: user.id });
|
||||
if (deletedAt == undefined) {
|
||||
dispatch('success');
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
min={0.001}
|
||||
max={0.1}
|
||||
desc="Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives."
|
||||
disabled={disabled || $featureFlags.duplicateDetection}
|
||||
disabled={disabled || !$featureFlags.duplicateDetection}
|
||||
isEdited={config.machineLearning.duplicateDetection.maxDistance !==
|
||||
savedConfig.machineLearning.duplicateDetection.maxDistance}
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
{#if group}
|
||||
<div class="grid">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => toggleAlbumGroupCollapsing(group.id)}
|
||||
class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
|
||||
aria-expanded={!isCollapsed}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||
<div class="p-2">
|
||||
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<th class="text-sm font-medium {option.columnStyle}">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
||||
on:click={handleSort}
|
||||
>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
{@const isCollapsed = isAlbumGroupCollapsed($albumViewSettings, albumGroup.id)}
|
||||
{@const iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90'}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => toggleAlbumGroupCollapsing(albumGroup.id)}
|
||||
class="flex w-full mt-4 rounded-md"
|
||||
aria-expanded={!isCollapsed}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getMyUserInfo,
|
||||
getMyUser,
|
||||
removeUserFromAlbum,
|
||||
type AlbumResponseDto,
|
||||
type UserResponseDto,
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
currentUser = await getMyUserInfo();
|
||||
currentUser = await getMyUser();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to refresh user');
|
||||
}
|
||||
@@ -141,6 +141,7 @@
|
||||
</div>
|
||||
{:else if user.id == currentUser?.id}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (selectedRemoveUser = user)}
|
||||
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
|
||||
>Leave</button
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Dropdown from '$lib/components/elements/dropdown.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
@@ -7,7 +6,7 @@
|
||||
import {
|
||||
AlbumUserRole,
|
||||
getAllSharedLinks,
|
||||
getAllUsers,
|
||||
searchUsers,
|
||||
type AlbumResponseDto,
|
||||
type AlbumUserAddDto,
|
||||
type SharedLinkResponseDto,
|
||||
@@ -36,10 +35,10 @@
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
onMount(async () => {
|
||||
await getSharedLinks();
|
||||
const data = await getAllUsers({ isAll: false });
|
||||
const data = await searchUsers();
|
||||
|
||||
// remove invalid users
|
||||
users = data.filter((user) => !(user.deletedAt || user.id === album.ownerId));
|
||||
// remove album owner
|
||||
users = data.filter((user) => user.id !== album.ownerId);
|
||||
|
||||
// Remove the existed shared users from the album
|
||||
for (const sharedUser of album.albumUsers) {
|
||||
@@ -122,7 +121,11 @@
|
||||
{#each users as user}
|
||||
{#if !Object.keys(selectedUsers).includes(user.id)}
|
||||
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
|
||||
<button on:click={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleToggle(user)}
|
||||
class="flex w-full place-items-center gap-4 p-4"
|
||||
>
|
||||
<UserAvatar {user} size="md" />
|
||||
<div class="text-left flex-grow">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
@@ -160,6 +163,7 @@
|
||||
|
||||
<div id="shared-buttons" class="mt-4 flex place-content-center place-items-center justify-around">
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
||||
on:click={() => dispatch('share')}
|
||||
>
|
||||
@@ -168,13 +172,13 @@
|
||||
</button>
|
||||
|
||||
{#if sharedLinks.length}
|
||||
<button
|
||||
<a
|
||||
href={AppRoute.SHARED_LINKS}
|
||||
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
||||
on:click={() => goto(AppRoute.SHARED_LINKS)}
|
||||
>
|
||||
<Icon path={mdiShareCircle} size={24} />
|
||||
<p class="text-sm">View links</p>
|
||||
</button>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
<div
|
||||
class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60"
|
||||
>
|
||||
<button class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
|
||||
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
|
||||
<div class="items-center justify-center">
|
||||
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
||||
</div>
|
||||
</button>
|
||||
<button on:click={() => dispatch('openActivityTab')}>
|
||||
<button type="button" on:click={() => dispatch('openActivityTab')}>
|
||||
<div class="flex gap-2 items-center justify-center">
|
||||
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
|
||||
{#if numberOfComments}
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-3 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-100 transition-colors"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
@@ -252,6 +253,7 @@
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-3 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-100 transition-colors"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => dispatch('album')}
|
||||
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
export let label: string;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="my-auto group hover:cursor-pointer" on:click={onClick}>
|
||||
<button
|
||||
class="mx-4 rounded-full p-3 text-gray-500 transition group-hover:bg-gray-500 group-hover:text-white"
|
||||
aria-label={label}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="my-auto mx-4 rounded-full p-3 text-gray-500 transition hover:bg-gray-500 hover:text-white"
|
||||
aria-label={label}
|
||||
on:click={onClick}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<!-- Select asset button -->
|
||||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||
<button
|
||||
type="button"
|
||||
on:click={onIconClickedHandler}
|
||||
class="absolute p-2 focus:outline-none"
|
||||
class:cursor-not-allowed={disabled}
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
{@const renderedOption = renderOption(option)}
|
||||
{@const buttonStyle = renderedOption.disabled ? '' : 'transition-all hover:bg-gray-300 dark:hover:bg-gray-800'}
|
||||
<button
|
||||
type="button"
|
||||
class="grid grid-cols-[36px,1fr] place-items-center p-2 disabled:opacity-40 {buttonStyle}"
|
||||
disabled={renderedOption.disabled}
|
||||
on:click={() => !renderedOption.disabled && handleSelectOption(option)}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div class="flex bg-gray-200 dark:bg-immich-dark-gray rounded-2xl h-full">
|
||||
{#each filters as filter, index}
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm px-4 {filter === selected
|
||||
? 'dark:bg-gray-700 bg-gray-300'
|
||||
: 'dark:hover:bg-gray-800 hover:bg-gray-300'} {index === 0 ? 'rounded-l-2xl' : ''} {index ===
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{#each showPeople as person (person.id)}
|
||||
{#if person.id !== editedPerson?.id}
|
||||
<div class="w-fit">
|
||||
<button class="w-[90px]" on:click={() => onReassign(person)}>
|
||||
<button type="button" class="w-[90px]" on:click={() => onReassign(person)}>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="relative rounded-lg transition-all"
|
||||
on:click={handleOnClicked}
|
||||
disabled={!selectable}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={potentialMergePeople.length === 0}
|
||||
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
||||
on:click={() => {
|
||||
@@ -71,13 +72,13 @@
|
||||
{:else}
|
||||
<div class="grid w-full grid-cols-1 gap-2">
|
||||
<div class="px-2">
|
||||
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
<button type="button" on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
||||
{#each potentialMergePeople as person (person.id)}
|
||||
<div class="h-24 w-24 md:h-28 md:w-28">
|
||||
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
|
||||
<button type="button" class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
|
||||
<ImageThumbnail
|
||||
border={true}
|
||||
circle
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import PasswordField from '../shared-components/password-field.svelte';
|
||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { updateMyUser } from '@immich/sdk';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
let errorMessage: string;
|
||||
let success: string;
|
||||
|
||||
@@ -31,13 +30,7 @@
|
||||
if (valid) {
|
||||
errorMessage = '';
|
||||
|
||||
await updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
password: String(password),
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
});
|
||||
await updateMyUser({ userUpdateMeDto: { password: String(password) } });
|
||||
|
||||
dispatch('success');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { convertToBytes } from '$lib/utils/byte-converter';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createUser } from '@immich/sdk';
|
||||
import { createUserAdmin } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import PasswordField from '../shared-components/password-field.svelte';
|
||||
@@ -49,8 +49,8 @@
|
||||
error = '';
|
||||
|
||||
try {
|
||||
await createUser({
|
||||
createUserDto: {
|
||||
await createUserAdmin({
|
||||
userAdminCreateDto: {
|
||||
email,
|
||||
password,
|
||||
shouldChangePassword,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { mdiAccountEditOutline } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { mdiAccountEditOutline } from '@mdi/js';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserAdminResponseDto;
|
||||
export let canResetPassword = true;
|
||||
export let newPassword: string;
|
||||
export let onClose: () => void;
|
||||
@@ -36,9 +36,9 @@
|
||||
const editUser = async () => {
|
||||
try {
|
||||
const { id, email, name, storageLabel } = user;
|
||||
await updateUser({
|
||||
updateUserDto: {
|
||||
id,
|
||||
await updateUserAdmin({
|
||||
id,
|
||||
userAdminUpdateDto: {
|
||||
email,
|
||||
name,
|
||||
storageLabel: storageLabel || '',
|
||||
@@ -56,9 +56,9 @@
|
||||
try {
|
||||
newPassword = generatePassword();
|
||||
|
||||
await updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
await updateUserAdmin({
|
||||
id: user.id,
|
||||
userAdminUpdateDto: {
|
||||
password: newPassword,
|
||||
shouldChangePassword: true,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import { mdiFolderSync } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { getAllUsers } from '@immich/sdk';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
let userOptions: { value: string; text: string }[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
const users = await getAllUsers({ isAll: true });
|
||||
const users = await searchUsersAdmin({});
|
||||
userOptions = users.map((user) => ({ value: user.id, text: user.name }));
|
||||
});
|
||||
|
||||
|
||||
@@ -182,10 +182,9 @@
|
||||
/>
|
||||
|
||||
{#each currentMemory.assets as _, index}
|
||||
<button
|
||||
<a
|
||||
class="relative w-full py-2"
|
||||
on:click={() =>
|
||||
goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${index}`)}
|
||||
href="?{QueryParameter.MEMORY_INDEX}={memoryIndex}&{QueryParameter.ASSET_INDEX}={index}"
|
||||
>
|
||||
<span class="absolute left-0 h-[2px] w-full bg-gray-500" />
|
||||
{#await resetPromise}
|
||||
@@ -196,7 +195,7 @@
|
||||
style:width={`${index < assetIndex ? 100 : index > assetIndex ? 0 : $progress * 100}%`}
|
||||
/>
|
||||
{/await}
|
||||
</button>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<div>
|
||||
@@ -214,7 +213,11 @@
|
||||
class:opacity-0={!galleryInView}
|
||||
class:opacity-100={galleryInView}
|
||||
>
|
||||
<button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })}
|
||||
disabled={!galleryInView}
|
||||
>
|
||||
<CircleIconButton title="Hide gallery" icon={mdiChevronUp} color="light" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -231,7 +234,12 @@
|
||||
class:opacity-0={!previousMemory}
|
||||
class:hover:opacity-70={previousMemory}
|
||||
>
|
||||
<button class="relative h-full w-full rounded-2xl" disabled={!previousMemory} on:click={toPreviousMemory}>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-full w-full rounded-2xl"
|
||||
disabled={!previousMemory}
|
||||
on:click={toPreviousMemory}
|
||||
>
|
||||
{#if previousMemory}
|
||||
<img
|
||||
class="h-full w-full rounded-2xl object-cover"
|
||||
@@ -304,7 +312,12 @@
|
||||
class:opacity-0={!nextMemory}
|
||||
class:hover:opacity-70={nextMemory}
|
||||
>
|
||||
<button class="relative h-full w-full rounded-2xl" on:click={toNextMemory} disabled={!nextMemory}>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-full w-full rounded-2xl"
|
||||
on:click={toNextMemory}
|
||||
disabled={!nextMemory}
|
||||
>
|
||||
{#if nextMemory}
|
||||
<img
|
||||
class="h-full w-full rounded-2xl object-cover"
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<div class="flex gap-4 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
class="w-1/2 aspect-square bg-immich-bg rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-dark-primary/80 border-immich-primary dark:border dark:border-transparent"
|
||||
on:click={() => ($colorTheme.value = Theme.LIGHT)}
|
||||
>
|
||||
@@ -34,6 +35,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-1/2 aspect-square bg-immich-dark-bg rounded-3xl dark:border-[3px] dark:border-immich-dark-primary/80 dark:border-immich-dark-primary border border-transparent"
|
||||
on:click={() => ($colorTheme.value = Theme.DARK)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { memoryStore } from '$lib/stores/memory.store';
|
||||
@@ -46,6 +45,7 @@
|
||||
{#if canScrollLeft}
|
||||
<div class="absolute left-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||
on:click={scrollLeft}
|
||||
>
|
||||
@@ -56,6 +56,7 @@
|
||||
{#if canScrollRight}
|
||||
<div class="absolute right-4 top-[6rem] z-20" transition:fade={{ duration: 200 }}>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-gray-500 bg-gray-100 p-2 text-gray-500 opacity-50 hover:opacity-100"
|
||||
on:click={scrollRight}
|
||||
>
|
||||
@@ -68,9 +69,9 @@
|
||||
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
||||
{#each $memoryStore as memory, index (memory.yearsAgo)}
|
||||
{#if memory.assets.length > 0}
|
||||
<button
|
||||
<a
|
||||
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
|
||||
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
|
||||
href="{AppRoute.MEMORY}?{QueryParameter.MEMORY_INDEX}={index}"
|
||||
>
|
||||
<img
|
||||
class="h-full w-full rounded-xl object-cover"
|
||||
@@ -84,7 +85,7 @@
|
||||
<div
|
||||
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
/>
|
||||
<div class="immich-scrollbar overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleNew}
|
||||
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}
|
||||
use:listNavigation={suggestionContainer}
|
||||
>
|
||||
<button class="w-full" on:click={() => (hideSuggestion = false)}>
|
||||
<button type="button" class="w-full" on:click={() => (hideSuggestion = false)}>
|
||||
<SearchBar
|
||||
placeholder="Search places"
|
||||
bind:name={searchWord}
|
||||
@@ -133,6 +133,7 @@
|
||||
{#if !hideSuggestion}
|
||||
{#each suggestedPlaces as place, index}
|
||||
<button
|
||||
type="button"
|
||||
class=" flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
|
||||
suggestedPlaces.length - 1
|
||||
? 'rounded-b-lg border-b'
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click
|
||||
class="w-full bg-slate-100 p-4 text-left text-sm font-medium text-immich-fg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg"
|
||||
role="menuitem"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<div id="immich-dropdown" class="relative">
|
||||
<button
|
||||
type="button"
|
||||
{disabled}
|
||||
on:click={toggle}
|
||||
aria-expanded={isOpen}
|
||||
@@ -52,6 +53,7 @@
|
||||
<div class="absolute mt-2 flex w-full flex-col">
|
||||
{#each options.options as option}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
selected = option;
|
||||
isOpen = false;
|
||||
|
||||
@@ -43,13 +43,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusTrap>
|
||||
<section
|
||||
role="presentation"
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
|
||||
>
|
||||
<section
|
||||
role="presentation"
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
|
||||
on:keydown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FocusTrap>
|
||||
<div
|
||||
class="z-[9999] max-w-[95vw] max-h-[95vh] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
||||
style="max-height: min(95vh, 900px);"
|
||||
@@ -72,5 +75,5 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</FocusTrap>
|
||||
</FocusTrap>
|
||||
</section>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteProfileImage, updateUser, type UserAvatarColor } from '@immich/sdk';
|
||||
import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
|
||||
import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -27,9 +27,8 @@
|
||||
await deleteProfileImage();
|
||||
}
|
||||
|
||||
$user = await updateUser({
|
||||
updateUserDto: {
|
||||
id: $user.id,
|
||||
$user = await updateMyUser({
|
||||
userUpdateMeDto: {
|
||||
email: $user.email,
|
||||
name: $user.name,
|
||||
avatarColor: color,
|
||||
@@ -93,6 +92,7 @@
|
||||
|
||||
<div class="mb-4 flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300"
|
||||
on:click={() => dispatch('logout')}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color}
|
||||
<button on:click={() => dispatch('choose', color)}>
|
||||
<button type="button" on:click={() => dispatch('choose', color)}>
|
||||
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
on:escape={() => (shouldShowAccountInfoPanel = false)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex"
|
||||
on:mouseover={() => (shouldShowAccountInfo = true)}
|
||||
on:focus={() => (shouldShowAccountInfo = true)}
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
{#if notification.button}
|
||||
<p class="pl-[28px] mt-2.5 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200"
|
||||
on:click={handleButtonClick}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
</script>
|
||||
|
||||
<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700">
|
||||
<button on:click={() => (isOpen = !isOpen)} class="flex w-full place-items-center justify-between text-left">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
on:click={() => (isOpen = !isOpen)}
|
||||
class="flex w-full place-items-center justify-between text-left"
|
||||
>
|
||||
<div>
|
||||
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
@@ -38,8 +43,7 @@
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-expanded={isOpen}
|
||||
<div
|
||||
class="immich-circle-icon-button flex place-content-center place-items-center rounded-full p-3 transition-all hover:bg-immich-primary/10 dark:text-immich-dark-fg hover:dark:bg-immich-dark-primary/20"
|
||||
>
|
||||
<svg
|
||||
@@ -55,7 +59,7 @@
|
||||
>
|
||||
<path d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<div class="left">
|
||||
{#if showResetToDefault}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => dispatch('reset', { default: true })}
|
||||
class="bg-none text-sm font-medium text-immich-primary hover:text-immich-primary/75 dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75"
|
||||
>
|
||||
@@ -26,6 +27,6 @@
|
||||
|
||||
<div class="right">
|
||||
<Button {disabled} size="sm" color="gray" on:click={() => dispatch('reset', { default: false })}>Reset</Button>
|
||||
<Button {disabled} size="sm" on:click={() => dispatch('save')}>Save</Button>
|
||||
<Button type="submit" {disabled} size="sm" on:click={() => dispatch('save')}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,4 +27,25 @@ describe('SettingInputField component', () => {
|
||||
await user.click(document.body);
|
||||
expect(numberInput.value).toEqual('100');
|
||||
});
|
||||
|
||||
it('allows emptying number inputs while editing', async () => {
|
||||
const { getByRole } = render(SettingInputField, {
|
||||
props: {
|
||||
label: 'test-number-input',
|
||||
inputType: SettingInputFieldType.NUMBER,
|
||||
value: 5,
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
const numberInput = getByRole('spinbutton') as HTMLInputElement;
|
||||
expect(numberInput.value).toEqual('5');
|
||||
|
||||
await user.click(numberInput);
|
||||
await user.keyboard('{Backspace}');
|
||||
expect(numberInput.value).toEqual('');
|
||||
|
||||
await user.click(document.body);
|
||||
expect(numberInput.value).toEqual('0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import type { FormEventHandler } from 'svelte/elements';
|
||||
import { fly } from 'svelte/transition';
|
||||
import PasswordField from '../password-field.svelte';
|
||||
|
||||
@@ -25,7 +26,9 @@
|
||||
export let isEdited = false;
|
||||
export let passwordAutocomplete: string = 'current-password';
|
||||
|
||||
const validateInput = () => {
|
||||
const handleChange: FormEventHandler<HTMLInputElement> = (e) => {
|
||||
value = e.currentTarget.value;
|
||||
|
||||
if (inputType === SettingInputFieldType.NUMBER) {
|
||||
let newValue = Number(value) || 0;
|
||||
if (newValue < min) {
|
||||
@@ -77,8 +80,7 @@
|
||||
{step}
|
||||
{required}
|
||||
{value}
|
||||
on:input={(e) => (value = e.currentTarget.value)}
|
||||
on:blur={validateInput}
|
||||
on:change={handleChange}
|
||||
{disabled}
|
||||
{title}
|
||||
/>
|
||||
|
||||
@@ -84,10 +84,11 @@
|
||||
</div>
|
||||
{#if uploadAsset.state === UploadState.ERROR}
|
||||
<div class="flex h-full flex-col place-content-evenly place-items-center justify-items-center pr-2">
|
||||
<button on:click={() => handleRetry(uploadAsset)} title="Retry upload" class="flex text-sm">
|
||||
<button type="button" on:click={() => handleRetry(uploadAsset)} title="Retry upload" class="flex text-sm">
|
||||
<span class="text-immich-dark-gray dark:text-immich-dark-fg"><Icon path={mdiRefresh} size="20" /></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => uploadAssetsStore.removeUploadAsset(uploadAsset.id)}
|
||||
title="Dismiss error"
|
||||
class="flex text-sm"
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
{:else}
|
||||
<div class="rounded-full">
|
||||
<button
|
||||
type="button"
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
on:click={() => (showDetail = true)}
|
||||
class="absolute -left-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-primary p-5 text-xs text-gray-200"
|
||||
@@ -136,6 +137,7 @@
|
||||
</button>
|
||||
{#if $hasError}
|
||||
<button
|
||||
type="button"
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
on:click={() => (showDetail = true)}
|
||||
class="absolute -right-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-error p-5 text-xs text-gray-200"
|
||||
@@ -144,6 +146,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
on:click={() => (showDetail = true)}
|
||||
class="flex h-16 w-16 place-content-center place-items-center rounded-full bg-gray-200 p-5 text-sm text-immich-primary shadow-lg dark:bg-gray-600 dark:text-immich-gray"
|
||||
|
||||
@@ -3,23 +3,18 @@
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { updateMyUser, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserAdminResponseDto;
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const data = await updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
memoriesEnabled: user.memoriesEnabled,
|
||||
},
|
||||
});
|
||||
const data = await updateMyUser({ userUpdateMeDto: { memoriesEnabled: user.memoriesEnabled } });
|
||||
|
||||
Object.assign(user, data);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { oauth } from '$lib/utils';
|
||||
import { type UserResponseDto } from '@immich/sdk';
|
||||
import { type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
@@ -10,7 +10,7 @@
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserAdminResponseDto;
|
||||
|
||||
let loading = true;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getAllUsers, getPartners, type UserResponseDto } from '@immich/sdk';
|
||||
import { searchUsers, getPartners, type UserResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
@@ -14,11 +14,10 @@
|
||||
const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: update endpoint to have a query param for deleted users
|
||||
let users = await getAllUsers({ isAll: false });
|
||||
let users = await searchUsers();
|
||||
|
||||
// remove invalid users
|
||||
users = users.filter((_user) => !(_user.deletedAt || _user.id === user.id));
|
||||
// remove current user
|
||||
users = users.filter((_user) => _user.id !== user.id);
|
||||
|
||||
// exclude partners from the list of users available for selection
|
||||
const partners = await getPartners({ direction: 'shared-by' });
|
||||
@@ -38,6 +37,7 @@
|
||||
{#if availableUsers.length > 0}
|
||||
{#each availableUsers as user}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => selectUser(user)}
|
||||
class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -3,23 +3,22 @@
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { updateUser } from '@immich/sdk';
|
||||
import SettingInputField, {
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { updateMyUser } from '@immich/sdk';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
let editedUser = cloneDeep($user);
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
try {
|
||||
const data = await updateUser({
|
||||
updateUserDto: {
|
||||
id: editedUser.id,
|
||||
const data = await updateMyUser({
|
||||
userUpdateMeDto: {
|
||||
email: editedUser.email,
|
||||
name: editedUser.name,
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{@const assetData = JSON.stringify(asset, null, 2)}
|
||||
|
||||
<div class="relative">
|
||||
<button on:click={() => onSelectAsset(asset)} class="block relative">
|
||||
<button type="button" on:click={() => onSelectAsset(asset)} class="block relative">
|
||||
<!-- THUMBNAIL-->
|
||||
<img
|
||||
src={getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
</script>
|
||||
|
||||
<a href={AppRoute.DUPLICATES}>
|
||||
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
|
||||
<p class="text-xs font-medium p-4">ORGANIZE YOUR LIBRARY</p>
|
||||
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
|
||||
<p class="text-xs font-medium p-4">ORGANIZE YOUR LIBRARY</p>
|
||||
|
||||
<button class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex gap-4 p-4">
|
||||
<span
|
||||
><Icon path={mdiContentDuplicate} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
|
||||
</span>
|
||||
Review duplicates
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
<a href={AppRoute.DUPLICATES} class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex gap-4 p-4">
|
||||
<span
|
||||
><Icon path={mdiContentDuplicate} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
|
||||
</span>
|
||||
Review duplicates
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { UserResponseDto } from '@immich/sdk';
|
||||
import type { UserAdminResponseDto } from '@immich/sdk';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const user = writable<UserResponseDto>();
|
||||
export const user = writable<UserAdminResponseDto>();
|
||||
|
||||
/**
|
||||
* Reset the store to its initial undefined value. Make sure to
|
||||
* only do this _after_ redirecting to an unauthenticated page.
|
||||
*/
|
||||
export const resetSavedUser = () => {
|
||||
user.set(undefined as unknown as UserResponseDto);
|
||||
user.set(undefined as unknown as UserAdminResponseDto);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
startOAuth,
|
||||
unlinkOAuthAccount,
|
||||
type SharedLinkResponseDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||
|
||||
@@ -264,7 +263,7 @@ export const oauth = {
|
||||
login: (location: Location) => {
|
||||
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
||||
},
|
||||
link: (location: Location): Promise<UserResponseDto> => {
|
||||
link: (location: Location) => {
|
||||
return linkOAuthAccount({ oAuthCallbackDto: { url: location.href } });
|
||||
},
|
||||
unlink: () => {
|
||||
@@ -292,4 +291,4 @@ export const handlePromiseError = <T>(promise: Promise<T>): void => {
|
||||
|
||||
export const s = (count: number) => (count === 1 ? '' : 's');
|
||||
|
||||
export const memoryLaneTitle = (yearsAgo: number) => `year${s(yearsAgo)} ago`;
|
||||
export const memoryLaneTitle = (yearsAgo: number) => `${yearsAgo} year${s(yearsAgo)} ago`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getMyUserInfo, getStorage } from '@immich/sdk';
|
||||
import { getMyUser, getStorage } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
import { AppRoute } from '../constants';
|
||||
@@ -15,7 +15,7 @@ export const loadUser = async () => {
|
||||
try {
|
||||
let loaded = get(user);
|
||||
if (!loaded && hasAuthCookie()) {
|
||||
loaded = await getMyUserInfo();
|
||||
loaded = await getMyUser();
|
||||
user.set(loaded);
|
||||
}
|
||||
return loaded;
|
||||
|
||||
+4
-2
@@ -510,6 +510,7 @@
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleSelectFromComputer}
|
||||
class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
|
||||
>
|
||||
@@ -577,13 +578,13 @@
|
||||
{/if}
|
||||
|
||||
<!-- owner -->
|
||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<button type="button" on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
{/each}
|
||||
@@ -620,6 +621,7 @@
|
||||
<div class="w-[300px]">
|
||||
<p class="text-xs dark:text-immich-dark-fg">ADD PHOTOS</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
|
||||
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||
import { getUserById } from '@immich/sdk';
|
||||
import { getUser } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
await authenticate();
|
||||
|
||||
const partner = await getUserById({ id: params.userId });
|
||||
const partner = await getUser({ id: params.userId });
|
||||
const asset = await getAssetInfoFromParam(params);
|
||||
return {
|
||||
asset,
|
||||
|
||||
@@ -502,6 +502,7 @@
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
{#each people as person, index (person.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="relative"
|
||||
on:click={() => (person.isHidden = !person.isHidden)}
|
||||
on:mouseenter={() => (eyeColorMap[person.id] = 'black')}
|
||||
|
||||
@@ -464,6 +464,7 @@
|
||||
{:else}
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center"
|
||||
title="Edit name"
|
||||
on:click={() => (isEditingName = true)}
|
||||
@@ -507,6 +508,7 @@
|
||||
<div bind:this={suggestionContainer}>
|
||||
{#each suggestedPeople as person, index (person.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
|
||||
suggestedPeople.length - 1
|
||||
? 'rounded-b-lg border-b'
|
||||
|
||||
@@ -68,10 +68,10 @@
|
||||
rel="noopener noreferrer"
|
||||
class="flex grow basis-0 justify-center p-4"
|
||||
>
|
||||
<button class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<div class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<Icon path={mdiMessage} size={24} />
|
||||
<p class="text-sm">Get Help</p>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -80,10 +80,10 @@
|
||||
rel="noopener noreferrer"
|
||||
class="flex grow basis-0 justify-center p-4"
|
||||
>
|
||||
<button class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<div class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<Icon path={mdiPartyPopper} size={24} />
|
||||
<p class="text-sm">Read Changelog</p>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -92,10 +92,10 @@
|
||||
rel="noopener noreferrer"
|
||||
class="flex grow basis-0 justify-center p-4"
|
||||
>
|
||||
<button class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<div class="flex flex-col place-content-center place-items-center gap-2">
|
||||
<Icon path={mdiCodeTags} size={24} />
|
||||
<p class="text-sm">Check Logs</p>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
deleteLibrary,
|
||||
getAllLibraries,
|
||||
getLibraryStatistics,
|
||||
getUserById,
|
||||
getUserAdmin,
|
||||
removeOfflineFiles,
|
||||
scanLibrary,
|
||||
updateLibrary,
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
const refreshStats = async (listIndex: number) => {
|
||||
stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
||||
owner[listIndex] = await getUserById({ id: libraries[listIndex].ownerId });
|
||||
owner[listIndex] = await getUserAdmin({ id: libraries[listIndex].ownerId });
|
||||
photos[listIndex] = stats[listIndex].photos;
|
||||
videos[listIndex] = stats[listIndex].videos;
|
||||
totalCount[listIndex] = stats[listIndex].total;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { getAllUsers } from '@immich/sdk';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate({ admin: true });
|
||||
await requestServerInfo();
|
||||
const allUsers = await getAllUsers({ isAll: false });
|
||||
const allUsers = await searchUsersAdmin({ withDeleted: false });
|
||||
|
||||
return {
|
||||
allUsers,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
@@ -17,28 +18,27 @@
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
|
||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||
import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let allUsers: UserResponseDto[] = [];
|
||||
let allUsers: UserAdminResponseDto[] = [];
|
||||
let shouldShowEditUserForm = false;
|
||||
let shouldShowCreateUserForm = false;
|
||||
let shouldShowPasswordResetSuccess = false;
|
||||
let shouldShowDeleteConfirmDialog = false;
|
||||
let shouldShowRestoreDialog = false;
|
||||
let selectedUser: UserResponseDto;
|
||||
let selectedUser: UserAdminResponseDto;
|
||||
let newPassword: string;
|
||||
|
||||
const refresh = async () => {
|
||||
allUsers = await getAllUsers({ isAll: false });
|
||||
allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||
};
|
||||
|
||||
const onDeleteSuccess = (userId: string) => {
|
||||
@@ -75,7 +75,7 @@
|
||||
shouldShowCreateUserForm = false;
|
||||
};
|
||||
|
||||
const editUserHandler = (user: UserResponseDto) => {
|
||||
const editUserHandler = (user: UserAdminResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowEditUserForm = true;
|
||||
};
|
||||
@@ -91,7 +91,7 @@
|
||||
shouldShowPasswordResetSuccess = true;
|
||||
};
|
||||
|
||||
const deleteUserHandler = (user: UserResponseDto) => {
|
||||
const deleteUserHandler = (user: UserAdminResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowDeleteConfirmDialog = true;
|
||||
};
|
||||
@@ -101,7 +101,7 @@
|
||||
shouldShowDeleteConfirmDialog = false;
|
||||
};
|
||||
|
||||
const restoreUserHandler = (user: UserResponseDto) => {
|
||||
const restoreUserHandler = (user: UserAdminResponseDto) => {
|
||||
selectedUser = user;
|
||||
shouldShowRestoreDialog = true;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { getAllUsers } from '@immich/sdk';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate({ admin: true });
|
||||
await requestServerInfo();
|
||||
const allUsers = await getAllUsers({ isAll: false });
|
||||
const allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||
|
||||
return {
|
||||
allUsers,
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
enter the new password below.
|
||||
</p>
|
||||
|
||||
<ChangePasswordForm user={$user} on:success={onSuccess} />
|
||||
<ChangePasswordForm on:success={onSuccess} />
|
||||
</FullscreenContainer>
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { UserAvatarColor, UserStatus, type UserResponseDto } from '@immich/sdk';
|
||||
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
|
||||
export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||
id: Sync.each(() => faker.string.uuid()),
|
||||
email: Sync.each(() => faker.internet.email()),
|
||||
name: Sync.each(() => faker.person.fullName()),
|
||||
storageLabel: Sync.each(() => faker.string.alphanumeric()),
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
||||
isAdmin: true,
|
||||
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
deletedAt: null,
|
||||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
memoriesEnabled: true,
|
||||
oauthId: '',
|
||||
avatarColor: UserAvatarColor.Primary,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: UserStatus.Active,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user