mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge remote-tracking branch 'origin/main' into keynav_timeline
This commit is contained in:
commit
b3752cc533
@ -362,6 +362,7 @@
|
||||
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
|
||||
"user_delete_immediately": "<b>{user}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.",
|
||||
"user_delete_immediately_checkbox": "Queue user and assets for immediate deletion",
|
||||
"user_details": "User Details",
|
||||
"user_management": "User Management",
|
||||
"user_password_has_been_reset": "The user's password has been reset:",
|
||||
"user_password_reset_description": "Please provide the temporary password to the user and inform them they will need to change the password at their next login.",
|
||||
@ -1290,6 +1291,7 @@
|
||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||
"notification_permission_list_tile_title": "Notification Permission",
|
||||
"notification_toggle_setting_description": "Enable email notifications",
|
||||
"email_notifications": "Email notifications",
|
||||
"notifications": "Notifications",
|
||||
"notifications_setting_description": "Manage notifications",
|
||||
"oauth": "OAuth",
|
||||
@ -1394,6 +1396,7 @@
|
||||
"previous_or_next_photo": "Previous or next photo",
|
||||
"primary": "Primary",
|
||||
"privacy": "Privacy",
|
||||
"profile": "Profile",
|
||||
"profile_drawer_app_logs": "Logs",
|
||||
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
||||
@ -1753,6 +1756,7 @@
|
||||
"storage": "Storage space",
|
||||
"storage_label": "Storage label",
|
||||
"storage_usage": "{used} of {available} used",
|
||||
"storage_quota": "Storage Quota",
|
||||
"submit": "Submit",
|
||||
"suggestions": "Suggestions",
|
||||
"sunrise_on_the_beach": "Sunrise on the beach",
|
||||
@ -1857,6 +1861,7 @@
|
||||
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
||||
"upload_to_immich": "Upload to Immich ({count})",
|
||||
"uploading": "Uploading",
|
||||
"id": "ID",
|
||||
"url": "URL",
|
||||
"usage": "Usage",
|
||||
"use_current_connection": "use current connection",
|
||||
@ -1864,6 +1869,8 @@
|
||||
"user": "User",
|
||||
"user_id": "User ID",
|
||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||
"created_at": "Created",
|
||||
"updated_at": "Updated",
|
||||
"user_purchase_settings": "Purchase",
|
||||
"user_purchase_settings_description": "Manage your purchase",
|
||||
"user_role_set": "Set {user} as {role}",
|
||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -253,6 +253,7 @@ Class | Method | HTTP request | Description
|
||||
*UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} |
|
||||
*UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} |
|
||||
*UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences |
|
||||
*UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics |
|
||||
*UsersAdminApi* | [**restoreUserAdmin**](doc//UsersAdminApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore |
|
||||
*UsersAdminApi* | [**searchUsersAdmin**](doc//UsersAdminApi.md#searchusersadmin) | **GET** /admin/users |
|
||||
*UsersAdminApi* | [**updateUserAdmin**](doc//UsersAdminApi.md#updateuseradmin) | **PUT** /admin/users/{id} |
|
||||
|
83
mobile/openapi/lib/api/users_admin_api.dart
generated
83
mobile/openapi/lib/api/users_admin_api.dart
generated
@ -211,6 +211,76 @@ class UsersAdminApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /admin/users/{id}/statistics' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<Response> getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/users/{id}/statistics'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (isFavorite != null) {
|
||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||
}
|
||||
if (isTrashed != null) {
|
||||
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
||||
}
|
||||
if (visibility != null) {
|
||||
queryParams.addAll(_queryParams('', 'visibility', visibility));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<AssetStatsResponseDto?> getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
|
||||
final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetStatsResponseDto',) as AssetStatsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /admin/users/{id}/restore' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
@ -262,8 +332,10 @@ class UsersAdminApi {
|
||||
/// Performs an HTTP 'GET /admin/users' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
Future<Response> searchUsersAdminWithHttpInfo({ bool? withDeleted, }) async {
|
||||
Future<Response> searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/users';
|
||||
|
||||
@ -274,6 +346,9 @@ class UsersAdminApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (withDeleted != null) {
|
||||
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
|
||||
}
|
||||
@ -294,9 +369,11 @@ class UsersAdminApi {
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id:
|
||||
///
|
||||
/// * [bool] withDeleted:
|
||||
Future<List<UserAdminResponseDto>?> searchUsersAdmin({ bool? withDeleted, }) async {
|
||||
final response = await searchUsersAdminWithHttpInfo( withDeleted: withDeleted, );
|
||||
Future<List<UserAdminResponseDto>?> searchUsersAdmin({ String? id, bool? withDeleted, }) async {
|
||||
final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
@ -345,6 +345,15 @@
|
||||
"get": {
|
||||
"operationId": "searchUsersAdmin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withDeleted",
|
||||
"required": false,
|
||||
@ -701,6 +710,72 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/admin/users/{id}/statistics": {
|
||||
"get": {
|
||||
"operationId": "getUserStatisticsAdmin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isFavorite",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isTrashed",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "visibility",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetStatsResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users (admin)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/albums": {
|
||||
"get": {
|
||||
"operationId": "getAllAlbums",
|
||||
|
@ -224,6 +224,11 @@ export type UserPreferencesUpdateDto = {
|
||||
sharedLinks?: SharedLinksUpdate;
|
||||
tags?: TagsUpdate;
|
||||
};
|
||||
export type AssetStatsResponseDto = {
|
||||
images: number;
|
||||
total: number;
|
||||
videos: number;
|
||||
};
|
||||
export type AlbumUserResponseDto = {
|
||||
role: AlbumUserRole;
|
||||
user: UserResponseDto;
|
||||
@ -462,11 +467,6 @@ export type AssetJobsDto = {
|
||||
assetIds: string[];
|
||||
name: AssetJobName;
|
||||
};
|
||||
export type AssetStatsResponseDto = {
|
||||
images: number;
|
||||
total: number;
|
||||
videos: number;
|
||||
};
|
||||
export type UpdateAssetDto = {
|
||||
dateTimeOriginal?: string;
|
||||
description?: string;
|
||||
@ -1502,13 +1502,15 @@ export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
||||
body: systemConfigSmtpDto
|
||||
})));
|
||||
}
|
||||
export function searchUsersAdmin({ withDeleted }: {
|
||||
export function searchUsersAdmin({ id, withDeleted }: {
|
||||
id?: string;
|
||||
withDeleted?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: UserAdminResponseDto[];
|
||||
}>(`/admin/users${QS.query(QS.explode({
|
||||
id,
|
||||
withDeleted
|
||||
}))}`, {
|
||||
...opts
|
||||
@ -1596,6 +1598,23 @@ export function restoreUserAdmin({ id }: {
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility }: {
|
||||
id: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
visibility?: AssetVisibility;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AssetStatsResponseDto;
|
||||
}>(`/admin/users/${encodeURIComponent(id)}/statistics${QS.query(QS.explode({
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
visibility
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getAllAlbums({ assetId, shared }: {
|
||||
assetId?: string;
|
||||
shared?: boolean;
|
||||
@ -3552,6 +3571,11 @@ export enum UserStatus {
|
||||
Removing = "removing",
|
||||
Deleted = "deleted"
|
||||
}
|
||||
export enum AssetVisibility {
|
||||
Archive = "archive",
|
||||
Timeline = "timeline",
|
||||
Hidden = "hidden"
|
||||
}
|
||||
export enum AlbumUserRole {
|
||||
Editor = "editor",
|
||||
Viewer = "viewer"
|
||||
@ -3661,11 +3685,6 @@ export enum Permission {
|
||||
AdminUserUpdate = "admin.user.update",
|
||||
AdminUserDelete = "admin.user.delete"
|
||||
}
|
||||
export enum AssetVisibility {
|
||||
Archive = "archive",
|
||||
Timeline = "timeline",
|
||||
Hidden = "hidden"
|
||||
}
|
||||
export enum AssetMediaStatus {
|
||||
Created = "created",
|
||||
Replaced = "replaced",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
@ -57,6 +58,16 @@ export class UserAdminController {
|
||||
return this.service.delete(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
|
||||
getUserStatisticsAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: AssetStatsDto,
|
||||
): Promise<AssetStatsResponseDto> {
|
||||
return this.service.getStatistics(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/preferences')
|
||||
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true })
|
||||
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
|
||||
|
@ -4,7 +4,7 @@ import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from
|
||||
import { User, UserAdmin } from 'src/database';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import { Optional, PinCode, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||
import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
@Optional()
|
||||
@ -67,6 +67,9 @@ export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||
export class UserAdminSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withDeleted?: boolean;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export class UserAdminCreateDto {
|
||||
|
@ -14,6 +14,7 @@ import { asUuid } from 'src/utils/database';
|
||||
type Upsert = Insertable<DbUserMetadata>;
|
||||
|
||||
export interface UserListFilter {
|
||||
id?: string;
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
@ -141,12 +142,13 @@ export class UserRepository {
|
||||
{ name: 'with deleted', params: [{ withDeleted: true }] },
|
||||
{ name: 'without deleted', params: [{ withDeleted: false }] },
|
||||
)
|
||||
getList({ withDeleted }: UserListFilter = {}) {
|
||||
getList({ id, withDeleted }: UserListFilter = {}) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(columns.userAdmin)
|
||||
.select(withMetadata)
|
||||
.$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
||||
.$if(!!id, (eb) => eb.where('users.id', '=', id!))
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
@ -18,7 +19,10 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
|
||||
@Injectable()
|
||||
export class UserAdminService extends BaseService {
|
||||
async search(auth: AuthDto, dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: dto.withDeleted });
|
||||
const users = await this.userRepository.getList({
|
||||
id: dto.id,
|
||||
withDeleted: dto.withDeleted,
|
||||
});
|
||||
return users.map((user) => mapUserAdmin(user));
|
||||
}
|
||||
|
||||
@ -109,6 +113,11 @@ export class UserAdminService extends BaseService {
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string, dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
|
||||
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
|
||||
return mapStats(stats);
|
||||
}
|
||||
|
||||
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
|
||||
await this.findOrFail(id, { withDeleted: true });
|
||||
const metadata = await this.userRepository.getMetadata(id);
|
||||
|
@ -3,6 +3,7 @@
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { getJobName } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@ -20,10 +21,9 @@
|
||||
mdiVideo,
|
||||
} from '@mdi/js';
|
||||
import type { Component } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import JobTile from './job-tile.svelte';
|
||||
import StorageMigrationDescription from './storage-migration-description.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
jobs: AllJobStatusResponseDto;
|
||||
@ -45,7 +45,7 @@
|
||||
|
||||
const handleConfirmCommand = async (jobId: JobName, dto: JobCommandDto) => {
|
||||
if (dto.force) {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_reprocess_all_faces'),
|
||||
});
|
||||
|
||||
|
@ -103,9 +103,7 @@
|
||||
class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
{#each stats.usageByUser as user (user.userId)}
|
||||
<tr
|
||||
class="flex h-[50px] w-full place-items-center text-center odd:bg-subtle even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
|
||||
>
|
||||
<tr class="flex h-[50px] w-full place-items-center text-center even:bg-subtle/20 odd:bg-subtle/80">
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
|
||||
<td class="w-1/4 text-ellipsis px-2 text-sm"
|
||||
>{user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)})</td
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { OAuthTokenEndpointAuthMethod, type SystemConfigDto } from '@immich/sdk';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -47,7 +47,7 @@
|
||||
</script>
|
||||
|
||||
{#if isConfirmOpen}
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('admin.disable_login')}
|
||||
onClose={(confirmed) => (confirmed ? handleSave(true) : (isConfirmOpen = false))}
|
||||
>
|
||||
@ -70,7 +70,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
|
@ -2,10 +2,10 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@ -191,7 +191,7 @@
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser}
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('album_remove_user')}
|
||||
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
|
||||
confirmText={$t('remove_user')}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@ -40,7 +40,7 @@
|
||||
onblur={handleUpdateName}
|
||||
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
|
||||
? 'hover:border-gray-400'
|
||||
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
|
||||
: 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
|
||||
type="text"
|
||||
bind:value={newAlbumName}
|
||||
disabled={!isOwned}
|
||||
|
@ -1,11 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||
import AssetGrid from '../photos-page/asset-grid.svelte';
|
||||
@ -13,14 +20,7 @@
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
|
||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import AlbumSummary from './album-summary.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
@ -101,14 +101,12 @@
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<main
|
||||
class="relative h-dvh overflow-hidden bg-immich-bg px-2 md:px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
|
||||
>
|
||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)]">
|
||||
<AssetGrid enableRouting={true} {album} {assetStore} {assetInteraction}>
|
||||
<section class="pt-8 md:pt-24 px-2 md:px-0">
|
||||
<!-- ALBUM TITLE -->
|
||||
<h1
|
||||
class="bg-immich-bg text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:bg-immich-dark-bg dark:text-immich-dark-primary"
|
||||
class="text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary"
|
||||
>
|
||||
{album.albumName}
|
||||
</h1>
|
||||
|
@ -31,7 +31,7 @@
|
||||
</script>
|
||||
|
||||
<tr
|
||||
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-subtle even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
||||
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
||||
onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||
{oncontextmenu}
|
||||
>
|
||||
|
@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
getMyUser,
|
||||
@ -13,10 +20,6 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
@ -140,7 +143,7 @@
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('album_leave')}
|
||||
prompt={$t('album_leave_confirmation', { values: { album: album.albumName } })}
|
||||
confirmText={$t('leave')}
|
||||
@ -149,7 +152,7 @@
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id}
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('album_remove_user')}
|
||||
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
|
||||
confirmText={$t('remove_user')}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { keepThisDeleteOthers } from '$lib/utils/asset-utils';
|
||||
import type { AssetResponseDto, StackResponseDto } from '@immich/sdk';
|
||||
import { mdiPinOutline } from '@mdi/js';
|
||||
import type { OnAction } from './action';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import type { OnAction } from './action';
|
||||
|
||||
interface Props {
|
||||
stack: StackResponseDto;
|
||||
@ -17,7 +17,7 @@
|
||||
let { stack, asset, onAction }: Props = $props();
|
||||
|
||||
const handleKeepThisDeleteOthers = async () => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('keep_this_delete_others'),
|
||||
prompt: $t('confirm_keep_this_delete_others'),
|
||||
confirmText: $t('delete_others'),
|
||||
|
@ -529,7 +529,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="detail-panel"
|
||||
class="row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="row-start-1 row-span-4 w-[360px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
|
||||
@ -540,7 +540,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="editor-panel"
|
||||
class="row-start-1 row-span-4 w-[400px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="row-start-1 row-span-4 w-[400px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<EditorPanel {asset} onUpdateSelectedType={handleUpdateSelectedEditType} onClose={closeEditor} />
|
||||
@ -589,7 +589,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="row-start-1 row-span-5 w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="row-start-1 row-span-5 w-[360px] md:w-[460px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
|
@ -16,7 +16,7 @@
|
||||
{#if downloadManager.isDownloading}
|
||||
<div
|
||||
transition:fly={{ x: -100, duration: 350 }}
|
||||
class="fixed bottom-10 start-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
|
||||
class="fixed bottom-10 start-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border p-4 text-sm shadow-sm bg-light"
|
||||
>
|
||||
<p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p>
|
||||
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { editTypes, showCancelConfirmDialog } from '$lib/stores/asset-editor.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
@ -67,11 +67,9 @@
|
||||
</section>
|
||||
|
||||
{#if $showCancelConfirmDialog}
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('editor_close_without_save_title')}
|
||||
prompt={$t('editor_close_without_save_prompt')}
|
||||
cancelText={$t('no')}
|
||||
cancelColor="secondary"
|
||||
confirmColor="danger"
|
||||
confirmText={$t('close')}
|
||||
onClose={(confirmed) => (confirmed ? onConfirm() : ($showCancelConfirmDialog = false))}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { notificationController } from '$lib/components/shared-components/notification/notification';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
@ -284,10 +284,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await dialogController.show({
|
||||
prompt: `Do you want to tag this face as ${person.name}?`,
|
||||
});
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: `Do you want to tag this face as ${person.name}?` });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto, getAllPeople } from '@immich/sdk';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
||||
import { AssetTypeEnum, getAllPeople, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
editedFace: AssetFaceResponseDto;
|
||||
@ -74,7 +74,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 z-[2001] h-full w-[360px] overflow-x-hidden p-2 bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
class="absolute top-0 z-[2001] h-full w-[360px] overflow-x-hidden p-2 dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
{#if !searchFaces}
|
||||
|
@ -8,6 +8,7 @@
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { ToggleVisibility } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@ -15,7 +16,6 @@
|
||||
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { ToggleVisibility } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
people: PersonResponseDto[];
|
||||
@ -134,7 +134,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
|
||||
<div class="flex flex-wrap gap-1 p-2 pb-8 md:px-8 mt-16">
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage}>
|
||||
{#snippet children({ person })}
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
|
@ -3,10 +3,12 @@
|
||||
import { page } from '$app/state';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
@ -16,8 +18,6 @@
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import PeopleList from './people-list.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
@ -69,10 +69,7 @@
|
||||
};
|
||||
|
||||
const handleMerge = async () => {
|
||||
const isConfirm = await dialogController.show({
|
||||
prompt: $t('merge_people_prompt'),
|
||||
});
|
||||
|
||||
const isConfirm = await modalManager.showDialog({ prompt: $t('merge_people_prompt') });
|
||||
if (!isConfirm) {
|
||||
return;
|
||||
}
|
||||
@ -99,7 +96,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-light"
|
||||
>
|
||||
<ControlAppBar onClose={onBack}>
|
||||
{#snippet leading()}
|
||||
@ -117,7 +114,7 @@
|
||||
>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section class="px-[70px] pt-[100px]">
|
||||
<section id="merge-face-selector">
|
||||
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
||||
<p class="mb-4 text-center uppercase dark:text-white">{$t('choose_matching_people_to_merge')}</p>
|
||||
|
@ -50,7 +50,7 @@
|
||||
searchedPeopleLocal = $bindable(),
|
||||
type,
|
||||
numberPeopleToSearch = maximumLengthSearchPeople,
|
||||
inputClass = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg',
|
||||
inputClass = 'w-full gap-2',
|
||||
showLoadingSpinner = $bindable(false),
|
||||
placeholder = $t('name_or_nickname'),
|
||||
onReset = () => {},
|
||||
|
@ -1,34 +1,34 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
createPerson,
|
||||
deleteFace,
|
||||
getFaces,
|
||||
reassignFacesById,
|
||||
AssetTypeEnum,
|
||||
type AssetFaceResponseDto,
|
||||
type PersonResponseDto,
|
||||
deleteFace,
|
||||
} from '@immich/sdk';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
|
||||
interface Props {
|
||||
assetId: string;
|
||||
@ -173,10 +173,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('confirm_delete_face', { values: { name: face.person.name } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
@ -194,7 +193,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 z-[2000] h-full w-[360px] overflow-x-hidden p-2 bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
class="absolute top-0 z-[2000] h-full w-[360px] overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -11,6 +11,7 @@
|
||||
} from '@immich/sdk';
|
||||
import { mdiMerge, mdiPlus } from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
@ -19,7 +20,6 @@
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import PeopleList from './people-list.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
assetIds: string[];
|
||||
@ -120,7 +120,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-light"
|
||||
>
|
||||
<ControlAppBar {onClose}>
|
||||
{#snippet leading()}
|
||||
@ -161,7 +161,7 @@
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{@render merge?.()}
|
||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section class="px-[70px] pt-[100px]">
|
||||
<section id="merge-face-selector relative">
|
||||
{#if selectedPerson !== null}
|
||||
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
||||
|
@ -177,9 +177,7 @@
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each validatedPaths as validatedPath, listIndex (validatedPath.importPath)}
|
||||
<tr
|
||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
listIndex % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-1/8 text-ellipsis ps-8 text-sm">
|
||||
{#if validatedPath.isValid}
|
||||
@ -215,9 +213,7 @@
|
||||
</tr>
|
||||
{/each}
|
||||
<tr
|
||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
importPaths.length % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-4/5 text-ellipsis px-4 text-sm">
|
||||
{#if importPaths.length === 0}
|
||||
|
@ -127,9 +127,7 @@
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each exclusionPatterns as exclusionPattern, listIndex (exclusionPattern)}
|
||||
<tr
|
||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
listIndex % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-3/4 text-ellipsis px-4 text-sm">{exclusionPattern}</td>
|
||||
<td class="w-1/4 text-ellipsis flex justify-center">
|
||||
@ -147,9 +145,7 @@
|
||||
</tr>
|
||||
{/each}
|
||||
<tr
|
||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
exclusionPatterns.length % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-3/4 text-ellipsis px-4 text-sm">
|
||||
{#if exclusionPatterns.length === 0}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { mdiCodeTags, mdiContentCopy, mdiMessage, mdiPartyPopper } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -22,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<div class="h-dvh w-dvw">
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<section>
|
||||
<div class="flex place-items-center border-b px-6 py-4 dark:border-b-immich-dark-gray">
|
||||
<a class="flex place-items-center gap-2 hover:cursor-pointer" href="/photos">
|
||||
<ImmichLogo class="h-[50px]" />
|
||||
@ -33,7 +33,7 @@
|
||||
<div class="fixed top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50">
|
||||
<div>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border shadow-sm dark:border-immich-dark-gray dark:text-immich-dark-fg bg-subtle/80"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-4">
|
||||
|
@ -51,7 +51,7 @@
|
||||
</header>
|
||||
<div
|
||||
tabindex="-1"
|
||||
class="relative grid grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg dark:bg-immich-dark-bg sidebar:grid-cols-[theme(spacing.64)_auto]
|
||||
class="relative grid grid-cols-[theme(spacing.0)_auto] overflow-hidden sidebar:grid-cols-[theme(spacing.64)_auto]
|
||||
{hideNavbar ? 'h-dvh' : 'h-[calc(100dvh-var(--navbar-height))]'}
|
||||
{hideNavbar ? 'pt-[var(--navbar-height)]' : ''}
|
||||
{hideNavbar ? 'max-md:pt-[var(--navbar-height-md)]' : ''}"
|
||||
|
@ -23,7 +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"
|
||||
class="light w-1/2 aspect-square bg-light rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-dark-primary/80 border-immich-primary dark:border dark:border-transparent"
|
||||
onclick={() => themeManager.setTheme(Theme.LIGHT)}
|
||||
>
|
||||
<div
|
||||
@ -35,7 +35,7 @@
|
||||
</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"
|
||||
class="dark w-1/2 aspect-square bg-light rounded-3xl dark:border-[3px] dark:border-immich-dark-primary/80 dark:border-immich-dark-primary border border-transparent"
|
||||
onclick={() => themeManager.setTheme(Theme.DARK)}
|
||||
>
|
||||
<div
|
||||
|
@ -4,12 +4,12 @@
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { getAlbumInfo, removeAssetFromAlbum, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { mdiDeleteOutline, mdiImageRemoveOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
@ -22,7 +22,7 @@
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const removeFromAlbum = async () => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().length } }),
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { removeSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { mdiDeleteOutline } from '@mdi/js';
|
||||
@ -18,7 +18,7 @@
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleRemove = async () => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('remove_assets_title'),
|
||||
prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().length } }),
|
||||
confirmText: $t('remove'),
|
||||
|
@ -1,20 +1,25 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import {
|
||||
type AssetStore,
|
||||
type AssetBucket,
|
||||
assetSnapshot,
|
||||
assetsSnapshot,
|
||||
type AssetStore,
|
||||
isSelectingAllAssets,
|
||||
} from '$lib/stores/assets-store.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { getDateLocaleString } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
<<<<<<< HEAD
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { scale } from 'svelte/transition';
|
||||
=======
|
||||
|
||||
>>>>>>> origin/main
|
||||
import { flip } from 'svelte/animate';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
|
||||
@ -126,7 +131,7 @@
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<div
|
||||
class="flex z-[100] pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
|
||||
class="flex z-[100] pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm"
|
||||
style:width={dateGroup.width + 'px'}
|
||||
>
|
||||
{#if !singleSelect && ((hoveredDateGroup === dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
|
||||
|
||||
interface Props {
|
||||
size: number;
|
||||
@ -23,7 +23,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('permanently_delete_assets_count', { values: { count: size } })}
|
||||
confirmText={$t('delete')}
|
||||
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
|
||||
@ -42,4 +42,4 @@
|
||||
<Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked />
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<div class="overflow-clip" style:height={height + 'px'}>
|
||||
<div
|
||||
class="flex z-[100] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
|
||||
class="flex z-[100] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
|
@ -87,7 +87,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<section>
|
||||
{#if sharedLink?.allowUpload || assets.length > 1}
|
||||
{#if assetInteraction.selectionActive}
|
||||
<AssetSelectControlBar
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import AlbumListItem from '../../asset-viewer/album-list-item.svelte';
|
||||
import NewAlbumListItem from './new-album-list-item.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { initInput } from '$lib/actions/focus';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
AlbumModalRowConverter,
|
||||
AlbumModalRowType,
|
||||
isSelectableRowType,
|
||||
} from '$lib/components/shared-components/album-selection/album-selection-utils';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import AlbumListItem from '../../asset-viewer/album-list-item.svelte';
|
||||
import NewAlbumListItem from './new-album-list-item.svelte';
|
||||
|
||||
let albums: AlbumResponseDto[] = $state([]);
|
||||
let recentAlbums: AlbumResponseDto[] = $state([]);
|
||||
@ -98,7 +98,7 @@
|
||||
{/each}
|
||||
{:else}
|
||||
<input
|
||||
class="border-b-4 border-immich-bg bg-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||
class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||
placeholder={$t('search')}
|
||||
{onkeydown}
|
||||
bind:value={search}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
import Combobox, { type ComboBoxOption } from './combobox.svelte';
|
||||
import ConfirmDialog from './dialog/confirm-dialog.svelte';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@ -142,7 +142,7 @@
|
||||
let date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
confirmColor="primary"
|
||||
{title}
|
||||
prompt="Please select a new date:"
|
||||
@ -170,4 +170,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -1,20 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { timeDebounceOnSearch } from '$lib/constants';
|
||||
import { lastChosenLocation } from '$lib/stores/asset-editor.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import ConfirmDialog from './dialog/confirm-dialog.svelte';
|
||||
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import SearchBar from '$lib/components/elements/search-bar.svelte';
|
||||
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import type Map from '$lib/components/shared-components/map/map.svelte';
|
||||
import { timeToLoadTheMap } from '$lib/constants';
|
||||
import { timeDebounceOnSearch, timeToLoadTheMap } from '$lib/constants';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { lastChosenLocation } from '$lib/stores/asset-editor.store';
|
||||
import { delay } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import SearchBar from '../elements/search-bar.svelte';
|
||||
import LoadingSpinner from './loading-spinner.svelte';
|
||||
interface Point {
|
||||
lng: number;
|
||||
lat: number;
|
||||
@ -112,7 +110,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
confirmColor="primary"
|
||||
title={$t('change_location')}
|
||||
size="medium"
|
||||
@ -208,4 +206,4 @@
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -32,7 +32,7 @@
|
||||
trailing,
|
||||
}: Props = $props();
|
||||
|
||||
let appBarBorder = $state('bg-immich-bg border border-transparent');
|
||||
let appBarBorder = $state('bg-light border border-transparent');
|
||||
|
||||
const onScroll = () => {
|
||||
if (window.scrollY > 80) {
|
||||
@ -42,7 +42,7 @@
|
||||
appBarBorder = 'border border-gray-600';
|
||||
}
|
||||
} else {
|
||||
appBarBorder = 'bg-immich-bg border border-transparent';
|
||||
appBarBorder = 'bg-light border border-transparent';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { dialogController } from './dialog';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
const { dialog } = dialogController;
|
||||
</script>
|
||||
|
||||
{#if $dialog}
|
||||
<ConfirmDialog {...$dialog} />
|
||||
{/if}
|
@ -1,42 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type DialogActions = {
|
||||
onClose: (confirmed: boolean) => void;
|
||||
};
|
||||
|
||||
type DialogOptions = {
|
||||
title?: string;
|
||||
prompt?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
hideCancelButton?: boolean;
|
||||
disable?: boolean;
|
||||
width?: 'wide' | 'narrow' | undefined;
|
||||
};
|
||||
|
||||
export type Dialog = DialogOptions & DialogActions;
|
||||
|
||||
function createDialogWrapper() {
|
||||
const dialog = writable<Dialog | undefined>();
|
||||
|
||||
async function show(options: DialogOptions) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const newDialog: Dialog = {
|
||||
...options,
|
||||
onClose: (confirmed) => {
|
||||
dialog.set(undefined);
|
||||
resolve(confirmed);
|
||||
},
|
||||
};
|
||||
|
||||
dialog.set(newDialog);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
dialog,
|
||||
show,
|
||||
};
|
||||
}
|
||||
|
||||
export const dialogController = createDialogWrapper();
|
@ -5,15 +5,13 @@
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import AvatarEditModal from '$lib/modals/AvatarEditModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
|
||||
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { NotificationType, notificationController } from '../notification/notification';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AvatarSelector from './avatar-selector.svelte';
|
||||
|
||||
interface Props {
|
||||
onLogout: () => void;
|
||||
@ -21,26 +19,6 @@
|
||||
}
|
||||
|
||||
let { onLogout, onClose = () => {} }: Props = $props();
|
||||
|
||||
let isShowSelectAvatar = $state(false);
|
||||
|
||||
const handleSaveProfile = async (color: UserAvatarColor) => {
|
||||
try {
|
||||
if ($user.profileImagePath !== '') {
|
||||
await deleteProfileImage();
|
||||
}
|
||||
|
||||
$user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } });
|
||||
isShowSelectAvatar = false;
|
||||
|
||||
notificationController.show({
|
||||
message: $t('saved_profile'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_profile'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -63,7 +41,7 @@
|
||||
class="border"
|
||||
size="12"
|
||||
padding="2"
|
||||
onclick={() => (isShowSelectAvatar = true)}
|
||||
onclick={() => modalManager.show(AvatarEditModal, {})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,7 +61,7 @@
|
||||
</Button>
|
||||
{#if $user.isAdmin}
|
||||
<Button
|
||||
href={AppRoute.ADMIN_USER_MANAGEMENT}
|
||||
href={AppRoute.ADMIN_USERS}
|
||||
onclick={onClose}
|
||||
color="dark-gray"
|
||||
size="sm"
|
||||
@ -111,7 +89,3 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isShowSelectAvatar}
|
||||
<AvatarSelector user={$user} onClose={() => (isShowSelectAvatar = false)} onChoose={handleSaveProfile} />
|
||||
{/if}
|
||||
|
@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import FullScreenModal from '../full-screen-modal.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
|
||||
interface Props {
|
||||
user: UserResponseDto;
|
||||
onClose: () => void;
|
||||
onChoose: (color: UserAvatarColor) => void;
|
||||
}
|
||||
|
||||
let { user, onClose, onChoose }: Props = $props();
|
||||
|
||||
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||
</script>
|
||||
|
||||
<FullScreenModal title={$t('select_avatar_color')} width="auto" {onClose}>
|
||||
<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 (color)}
|
||||
<button type="button" onclick={() => onChoose(color)}>
|
||||
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
@ -55,7 +55,7 @@
|
||||
>
|
||||
<SkipLink text={$t('skip_to_content')} />
|
||||
<div
|
||||
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg sidebar:grid-cols-[theme(spacing.64)_auto]"
|
||||
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b py-2 dark:border-b-immich-dark-gray sidebar:grid-cols-[theme(spacing.64)_auto]"
|
||||
>
|
||||
<div class="flex flex-row gap-1 mx-4 items-center">
|
||||
<IconButton
|
||||
|
@ -446,7 +446,7 @@
|
||||
aria-valuemax={toScrollY(1)}
|
||||
aria-valuemin={toScrollY(0)}
|
||||
data-id="immich-scrubbable-scrollbar"
|
||||
class="absolute end-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
|
||||
class="absolute end-0 z-[1] select-none hover:cursor-row-resize"
|
||||
style:padding-top={PADDING_TOP + 'px'}
|
||||
style:padding-bottom={PADDING_BOTTOM + 'px'}
|
||||
style:width
|
||||
@ -464,7 +464,7 @@
|
||||
class={[
|
||||
{ 'border-b-2': isDragging },
|
||||
{ 'rounded-bl-md': !isDragging },
|
||||
'truncate opacity-85 pointer-events-none absolute end-0 z-[100] min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary bg-immich-bg py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg',
|
||||
'bg-light truncate opacity-85 pointer-events-none absolute end-0 z-[100] min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg',
|
||||
]}
|
||||
style:top="{hoverY + 2}px"
|
||||
>
|
||||
@ -506,7 +506,7 @@
|
||||
{#if assetStore.scrolling && scrollHoverLabel && !isHover}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-immich-bg/80 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray/80 dark:text-immich-dark-fg"
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/70 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
|
||||
>
|
||||
{scrollHoverLabel}
|
||||
</p>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</script>
|
||||
|
||||
<SideBarSection ariaLabel={$t('primary')}>
|
||||
<SideBarLink title={$t('users')} routeId={AppRoute.ADMIN_USER_MANAGEMENT} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title={$t('users')} routeId={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title={$t('jobs')} routeId={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<SideBarLink title={$t('settings')} routeId={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<SideBarLink title={$t('external_libraries')} routeId={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
|
@ -35,7 +35,7 @@
|
||||
id="sidebar"
|
||||
aria-label={ariaLabel}
|
||||
tabindex="-1"
|
||||
class="immich-scrollbar relative z-auto w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
|
||||
class="immich-scrollbar relative z-auto w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden pt-8 transition-all duration-200"
|
||||
class:shadow-2xl={isExpanded}
|
||||
class:dark:border-e-immich-dark-gray={isExpanded}
|
||||
class:border-r={isExpanded}
|
||||
|
@ -40,11 +40,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
in:fade={{ duration: 250 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="flex flex-col rounded-lg bg-immich-bg text-xs dark:bg-immich-dark-bg p-2 gap-1"
|
||||
>
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 100 }} class="flex flex-col rounded-lg text-xs p-2 gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-center">
|
||||
{#if uploadAsset.state === UploadState.PENDING}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { deleteAllSessions, deleteSession, getSessions, type SessionResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import DeviceCard from './device-card.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { Button } from '@immich/ui';
|
||||
|
||||
interface Props {
|
||||
devices: SessionResponseDto[];
|
||||
@ -19,10 +19,7 @@
|
||||
let otherDevices = $derived(devices.filter((device) => !device.current));
|
||||
|
||||
const handleDelete = async (device: SessionResponseDto) => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
prompt: $t('logout_this_device_confirmation'),
|
||||
});
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('logout_this_device_confirmation') });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
@ -38,7 +35,7 @@
|
||||
};
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
const isConfirmed = await dialogController.show({ prompt: $t('logout_all_device_confirmation') });
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('logout_all_device_confirmation') });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
@ -81,7 +80,7 @@
|
||||
};
|
||||
|
||||
const handleRemovePartner = async (partner: PartnerResponseDto) => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('stop_photo_sharing'),
|
||||
prompt: $t('stop_photo_sharing_description', { values: { partner: partner.name } }),
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import ApiKeyModal from '$lib/modals/ApiKeyModal.svelte';
|
||||
import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
|
||||
@ -88,7 +87,7 @@
|
||||
};
|
||||
|
||||
const handleDelete = async (key: ApiKeyResponseDto) => {
|
||||
const isConfirmed = await dialogController.show({ prompt: $t('delete_api_key_prompt') });
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('delete_api_key_prompt') });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
@ -125,11 +124,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each keys as key, index (key.id)}
|
||||
{#each keys as key (key.id)}
|
||||
<tr
|
||||
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
index % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-1/3 text-ellipsis px-4 text-sm">{key.name}</td>
|
||||
<td class="w-1/3 text-ellipsis px-4 text-sm"
|
||||
|
@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||
import {
|
||||
deleteServerLicense as deleteServerProductKey,
|
||||
deleteUserLicense as deleteIndividualProductKey,
|
||||
deleteServerLicense as deleteServerProductKey,
|
||||
getAboutInfo,
|
||||
getMyUser,
|
||||
getServerLicense,
|
||||
isHttpError,
|
||||
type LicenseResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiKey } from '@mdi/js';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiKey } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
const { isPurchased } = purchaseStore;
|
||||
|
||||
let isServerProduct = $state(false);
|
||||
@ -62,11 +62,10 @@
|
||||
|
||||
const removeIndividualProductKey = async () => {
|
||||
try {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('purchase_remove_product_key'),
|
||||
prompt: $t('purchase_remove_product_key_prompt'),
|
||||
confirmText: $t('remove'),
|
||||
cancelText: $t('cancel'),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
@ -82,11 +81,10 @@
|
||||
|
||||
const removeServerProductKey = async () => {
|
||||
try {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('purchase_remove_server_product_key'),
|
||||
prompt: $t('purchase_remove_server_product_key_prompt'),
|
||||
confirmText: $t('remove'),
|
||||
cancelText: $t('cancel'),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
|
@ -57,7 +57,7 @@
|
||||
|
||||
{#snippet row(viewName: string, stats: AssetStatsResponseDto)}
|
||||
<tr
|
||||
class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg odd:bg-immich-bg even:bg-subtle odd:dark:bg-immich-dark-gray/50 even:dark:bg-immich-dark-gray/75"
|
||||
class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-1/4 px-4 text-sm">{viewName}</td>
|
||||
<td class="w-1/4 px-4 text-sm">{stats.images.toLocaleString($locale)}</td>
|
||||
@ -95,7 +95,7 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-start mt-4">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||
class="mb-4 flex h-12 w-full rounded-md border text-immich-primary dark:border-immich-dark-gray bg-subtle dark:text-immich-dark-primary"
|
||||
>
|
||||
<tr class="flex w-full place-items-center text-sm font-medium text-center">
|
||||
<th class="w-1/2">{$t('owned')}</th>
|
||||
@ -103,9 +103,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<tr
|
||||
class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-bg dark:bg-immich-dark-gray/50"
|
||||
>
|
||||
<tr class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg bg-subtle/20">
|
||||
<td class="w-1/2 px-4 text-sm">{albumStats.owned.toLocaleString($locale)}</td>
|
||||
<td class="w-1/2 px-4 text-sm">{albumStats.shared.toLocaleString($locale)}</td>
|
||||
</tr>
|
||||
|
@ -13,7 +13,7 @@ export enum AssetAction {
|
||||
}
|
||||
|
||||
export enum AppRoute {
|
||||
ADMIN_USER_MANAGEMENT = '/admin/user-management',
|
||||
ADMIN_USERS = '/admin/users',
|
||||
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
|
||||
ADMIN_SETTINGS = '/admin/system-settings',
|
||||
ADMIN_STATS = '/admin/server-status',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { mount, unmount, type Component, type ComponentProps } from 'svelte';
|
||||
|
||||
type OnCloseData<T> = T extends { onClose: (data?: infer R) => void } ? R : never;
|
||||
@ -34,8 +34,8 @@ class ModalManager {
|
||||
};
|
||||
}
|
||||
|
||||
openDialog(options: Omit<ComponentProps<typeof ConfirmDialog>, 'onClose'>) {
|
||||
return this.show(ConfirmDialog, options);
|
||||
showDialog(options: Omit<ComponentProps<typeof ConfirmModal>, 'onClose'>) {
|
||||
return this.show(ConfirmModal, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
47
web/src/lib/modals/AvatarEditModal.svelte
Normal file
47
web/src/lib/modals/AvatarEditModal.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteProfileImage, updateMyUser, UserAvatarColor } from '@immich/sdk';
|
||||
import { Modal, ModalBody } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { onClose }: Props = $props();
|
||||
|
||||
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||
|
||||
const onSave = async (color: UserAvatarColor) => {
|
||||
try {
|
||||
if ($user.profileImagePath !== '') {
|
||||
await deleteProfileImage();
|
||||
}
|
||||
|
||||
notificationController.show({ message: $t('saved_profile'), type: NotificationType.Info });
|
||||
|
||||
$user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_profile'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title={$t('select_avatar_color')} size="medium" {onClose}>
|
||||
<ModalBody>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color (color)}
|
||||
<button type="button" onclick={() => onSave(color)}>
|
||||
<UserAvatar label={color} user={$user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
@ -8,9 +8,6 @@
|
||||
prompt?: string;
|
||||
confirmText?: string;
|
||||
confirmColor?: Color;
|
||||
cancelText?: string;
|
||||
cancelColor?: Color;
|
||||
hideCancelButton?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'medium';
|
||||
onClose: (confirmed: boolean) => void;
|
||||
@ -22,9 +19,6 @@
|
||||
prompt = $t('are_you_sure_to_do_this'),
|
||||
confirmText = $t('confirm'),
|
||||
confirmColor = 'danger',
|
||||
cancelText = $t('cancel'),
|
||||
cancelColor = 'secondary',
|
||||
hideCancelButton = false,
|
||||
disabled = false,
|
||||
size = 'small',
|
||||
onClose,
|
||||
@ -44,12 +38,10 @@
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div class="flex gap-3 w-full my-3">
|
||||
{#if !hideCancelButton}
|
||||
<Button shape="round" color={cancelColor} fullWidth onclick={() => onClose(false)}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex gap-3 w-full">
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
|
||||
{$t('cancel')}
|
||||
</Button>
|
||||
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
|
||||
{confirmText}
|
||||
</Button>
|
@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createJob, ManualJobName } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -44,7 +44,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
confirmColor="primary"
|
||||
title={$t('admin.create_job')}
|
||||
disabled={!selectedJob}
|
||||
@ -62,4 +62,4 @@
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { Code, IconButton, Text } from '@immich/ui';
|
||||
import { mdiContentCopy } from '@mdi/js';
|
||||
import { Button, Code, IconButton, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
|
||||
import { mdiCheck, mdiContentCopy } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
@ -13,14 +12,14 @@
|
||||
const { onClose, newPassword }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<Modal
|
||||
title={$t('password_reset_success')}
|
||||
confirmText={$t('done')}
|
||||
{onClose}
|
||||
hideCancelButton={true}
|
||||
confirmColor="success"
|
||||
icon={mdiCheck}
|
||||
onClose={() => onClose()}
|
||||
size="small"
|
||||
class="bg-light text-dark"
|
||||
>
|
||||
{#snippet promptSnippet()}
|
||||
<ModalBody>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Text>{$t('admin.user_password_has_been_reset')}</Text>
|
||||
|
||||
@ -39,5 +38,13 @@
|
||||
|
||||
<Text>{$t('admin.user_password_reset_description')}</Text>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div class="flex gap-3 w-full">
|
||||
<Button shape="round" color="primary" fullWidth onclick={() => onClose()}>
|
||||
{$t('done')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
@ -39,7 +39,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('delete_user')}
|
||||
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
|
||||
onClose={(confirmed) => (confirmed ? handleDeleteUser() : onClose())}
|
||||
@ -98,4 +98,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -1,40 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import { mdiAccountEditOutline, mdiLockSmart, mdiOnepassword } from '@mdi/js';
|
||||
import { mdiAccountEditOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
user: UserAdminResponseDto;
|
||||
canResetPassword?: boolean;
|
||||
onClose: (
|
||||
data?:
|
||||
| { action: 'update'; data: UserAdminResponseDto }
|
||||
| { action: 'resetPassword'; data: string }
|
||||
| { action: 'resetPinCode' },
|
||||
) => void;
|
||||
onClose: (data?: UserAdminResponseDto) => void;
|
||||
}
|
||||
|
||||
let { user, canResetPassword = true, onClose }: Props = $props();
|
||||
let { user, onClose }: Props = $props();
|
||||
|
||||
let quotaSize = $state(user.quotaSizeInBytes === null ? null : convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB));
|
||||
let newPassword = $state<string>('');
|
||||
|
||||
const previousQutoa = user.quotaSizeInBytes;
|
||||
const previousQuota = user.quotaSizeInBytes;
|
||||
|
||||
let quotaSizeWarning = $derived(
|
||||
previousQutoa !== convertToBytes(Number(quotaSize), ByteUnit.GiB) &&
|
||||
previousQuota !== convertToBytes(Number(quotaSize), ByteUnit.GiB) &&
|
||||
!!quotaSize &&
|
||||
userInteraction.serverInfo &&
|
||||
convertToBytes(Number(quotaSize), ByteUnit.GiB) > userInteraction.serverInfo.diskSizeRaw,
|
||||
);
|
||||
|
||||
const editUser = async () => {
|
||||
const handleEditUser = async () => {
|
||||
try {
|
||||
const { id, email, name, storageLabel } = user;
|
||||
const newUser = await updateUserAdmin({
|
||||
@ -47,76 +39,15 @@
|
||||
},
|
||||
});
|
||||
|
||||
onClose({ action: 'update', data: newUser });
|
||||
onClose(newUser);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_user'));
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = async () => {
|
||||
const isConfirmed = await modalManager.openDialog({
|
||||
prompt: $t('admin.confirm_user_password_reset', { values: { user: user.name } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newPassword = generatePassword();
|
||||
|
||||
await updateUserAdmin({
|
||||
id: user.id,
|
||||
userAdminUpdateDto: {
|
||||
password: newPassword,
|
||||
shouldChangePassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
onClose({ action: 'resetPassword', data: newPassword });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_password'));
|
||||
}
|
||||
};
|
||||
|
||||
const resetUserPincode = async () => {
|
||||
const isConfirmed = await modalManager.openDialog({
|
||||
prompt: $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
||||
|
||||
onClose({ action: 'resetPinCode' });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO move password reset server-side
|
||||
function generatePassword(length: number = 16) {
|
||||
let generatedPassword = '';
|
||||
|
||||
const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
randomNumber = randomNumber / 2 ** 32;
|
||||
randomNumber = Math.floor(randomNumber * characterSet.length);
|
||||
|
||||
generatedPassword += characterSet[randomNumber];
|
||||
}
|
||||
|
||||
return generatedPassword;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await editUser();
|
||||
await handleEditUser();
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -172,34 +103,11 @@
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div class="w-full">
|
||||
<div class="flex gap-3 w-full">
|
||||
{#if canResetPassword}
|
||||
<Button
|
||||
shape="round"
|
||||
color="warning"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
onclick={resetPassword}
|
||||
leadingIcon={mdiOnepassword}
|
||||
>
|
||||
{$t('reset_password')}</Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
color="warning"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
onclick={resetUserPincode}
|
||||
leadingIcon={mdiLockSmart}>{$t('reset_pin_code')}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-4">
|
||||
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
|
||||
</div>
|
||||
<div class="flex gap-3 w-full">
|
||||
<Button shape="round" color="secondary" fullWidth form="edit-user-form" onclick={() => onClose()}
|
||||
>{$t('cancel')}</Button
|
||||
>
|
||||
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -22,7 +22,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
<ConfirmModal
|
||||
title={$t('restore_user')}
|
||||
confirmText={$t('continue')}
|
||||
confirmColor="success"
|
||||
@ -37,4 +37,4 @@
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
</ConfirmModal>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import {
|
||||
AlbumFilter,
|
||||
AlbumGroupBy,
|
||||
@ -213,7 +213,7 @@ export const confirmAlbumDelete = async (album: AlbumResponseDto) => {
|
||||
const description = $t('album_delete_confirmation_description');
|
||||
const prompt = `${confirmation} ${description}`;
|
||||
|
||||
return dialogController.show({ prompt });
|
||||
return modalManager.showDialog({ prompt });
|
||||
};
|
||||
|
||||
interface AlbumSortOption {
|
||||
|
@ -588,9 +588,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<main
|
||||
class="relative h-dvh overflow-hidden bg-immich-bg px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
|
||||
>
|
||||
<main class="relative h-dvh overflow-hidden px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)]">
|
||||
<AssetGrid
|
||||
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
||||
{album}
|
||||
@ -678,7 +676,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (viewMode = AlbumPageViewMode.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"
|
||||
class="mt-5 bg-subtle flex w-full place-items-center gap-6 rounded-md border px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
>
|
||||
<span class="text-text-immich-primary dark:text-immich-dark-primary"
|
||||
><Icon path={mdiPlus} size="24" />
|
||||
@ -709,7 +707,7 @@
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="z-[2] w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-s-immich-dark-gray dark:bg-immich-dark-bg"
|
||||
class="z-[2] w-[360px] md:w-[460px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
|
@ -72,7 +72,7 @@
|
||||
{#if $featureFlags.loaded && $featureFlags.map}
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<div class="isolate h-full w-full">
|
||||
<Map hash onSelect={onViewAssets} rounded />
|
||||
<Map hash onSelect={onViewAssets} />
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
<Portal target="body">
|
||||
|
@ -42,7 +42,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<main class="grid h-dvh bg-immich-bg pt-18 dark:bg-immich-dark-bg">
|
||||
<main class="grid h-dvh pt-18">
|
||||
{#if assetInteraction.selectionActive}
|
||||
<AssetSelectControlBar
|
||||
assets={assetInteraction.selectedAssets}
|
||||
|
@ -458,7 +458,7 @@
|
||||
<dialog
|
||||
open
|
||||
transition:fly={{ y: innerHeight, duration: 150, easing: quintOut, opacity: 0 }}
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
class="absolute start-0 top-0 z-[9999] h-full w-full bg-light"
|
||||
aria-modal="true"
|
||||
aria-labelledby="manage-visibility-title"
|
||||
use:focusTrap
|
||||
|
@ -487,7 +487,7 @@
|
||||
</header>
|
||||
|
||||
<main
|
||||
class="relative h-dvh overflow-hidden bg-immich-bg tall:ms-4 md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
|
||||
class="relative h-dvh overflow-hidden tall:ms-4 md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)]"
|
||||
use:scrollMemoryClearer={{
|
||||
routeStartsWith: AppRoute.PEOPLE,
|
||||
beforeClear: () => {
|
||||
|
@ -291,10 +291,7 @@
|
||||
{:else}
|
||||
<div class="fixed z-[100] top-0 start-0 w-full">
|
||||
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||
<div
|
||||
class="-z-[1] bg-immich-bg dark:bg-immich-dark-bg"
|
||||
style="position:absolute;top:0;left:0;right:0;bottom:0;"
|
||||
></div>
|
||||
<div class="-z-[1] bg-light" style="position:absolute;top:0;left:0;right:0;bottom:0;"></div>
|
||||
<div class="w-full flex-1 ps-4">
|
||||
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
||||
</div>
|
||||
|
@ -4,17 +4,17 @@
|
||||
import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||
import PasswordField from '$lib/components/shared-components/password-field.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
|
||||
import type { PageData } from './$types';
|
||||
import { setSharedLink } from '$lib/utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { setSharedLink } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
|
||||
import { tick } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@ -70,7 +70,7 @@
|
||||
</ControlAppBar>
|
||||
</header>
|
||||
<main
|
||||
class="relative h-dvh overflow-hidden bg-immich-bg px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||
class="relative h-dvh overflow-hidden px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] sm:px-12 md:px-24 lg:px-40"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center mt-20">
|
||||
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div>
|
||||
|
@ -3,13 +3,13 @@
|
||||
import { page } from '$app/state';
|
||||
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
@ -35,7 +35,7 @@
|
||||
});
|
||||
|
||||
const handleDeleteLink = async (id: string) => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: $t('delete_shared_link'),
|
||||
prompt: $t('confirm_delete_shared_link'),
|
||||
confirmText: $t('delete'),
|
||||
|
@ -4,7 +4,6 @@
|
||||
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
|
||||
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
@ -16,15 +15,16 @@
|
||||
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, Text } from '@immich/ui';
|
||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@ -116,11 +116,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirm = await dialogController.show({
|
||||
const isConfirm = await modalManager.showDialog({
|
||||
title: $t('delete_tag'),
|
||||
prompt: $t('delete_tag_confirmation_prompt', { values: { tagName: tag.value } }),
|
||||
confirmText: $t('delete'),
|
||||
cancelText: $t('cancel'),
|
||||
});
|
||||
|
||||
if (!isConfirm) {
|
||||
|
@ -7,13 +7,13 @@
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||
@ -43,10 +43,7 @@
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEmptyTrash = async () => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
prompt: $t('empty_trash_confirmation'),
|
||||
});
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
@ -64,10 +61,7 @@
|
||||
};
|
||||
|
||||
const handleRestoreTrash = async () => {
|
||||
const isConfirmed = await dialogController.show({
|
||||
prompt: $t('assets_restore_confirmation'),
|
||||
});
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('assets_restore_confirmation') });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import DuplicatesModal from '$lib/components/shared-components/duplicates-modal.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
@ -55,7 +54,7 @@
|
||||
let hasDuplicates = $derived(duplicates.length > 0);
|
||||
const withConfirmation = async (callback: () => Promise<void>, prompt?: string, confirmText?: string) => {
|
||||
if (prompt && confirmText) {
|
||||
const isConfirmed = await dialogController.show({ prompt, confirmText });
|
||||
const isConfirmed = await modalManager.showDialog({ prompt, confirmText });
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte';
|
||||
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
|
||||
import DialogWrapper from '$lib/components/shared-components/dialog/dialog-wrapper.svelte';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
@ -122,7 +121,6 @@
|
||||
<DownloadPanel />
|
||||
<UploadPanel />
|
||||
<NotificationList />
|
||||
<DialogWrapper />
|
||||
|
||||
{#if $user?.isAdmin}
|
||||
<VersionAnnouncementBox />
|
||||
|
@ -3,5 +3,5 @@ import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (() => {
|
||||
redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
|
||||
redirect(302, AppRoute.ADMIN_USERS);
|
||||
}) satisfies PageLoad;
|
||||
|
@ -7,13 +7,13 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { ByteUnit, getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@ -210,7 +210,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_delete_library', { values: { library: library.name } }),
|
||||
});
|
||||
|
||||
@ -221,10 +221,9 @@
|
||||
await refreshStats(index);
|
||||
const assetCount = totalCount[index];
|
||||
if (assetCount > 0) {
|
||||
const isConfirmed = await dialogController.show({
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_delete_library_assets', { values: { count: assetCount } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
@ -298,9 +297,7 @@
|
||||
<tbody class="block overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#each libraries as library, index (library.id)}
|
||||
<tr
|
||||
class={`grid grid-cols-6 h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
|
||||
index % 2 == 0 ? 'bg-subtle' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
class="grid grid-cols-6 h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="text-ellipsis px-4 text-sm">{library.name}</td>
|
||||
<td class="text-ellipsis px-4 text-sm">
|
||||
|
@ -1,18 +1,5 @@
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate({ admin: true });
|
||||
await requestServerInfo();
|
||||
const allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
allUsers,
|
||||
meta: {
|
||||
title: $t('admin.user_management'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
export const load = (() => redirect(307, AppRoute.ADMIN_USERS)) satisfies PageLoad;
|
||||
|
@ -6,7 +6,7 @@
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import PasswordResetSuccess from '$lib/forms/password-reset-success.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
||||
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||
@ -18,7 +18,7 @@
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { Button, IconButton, Link } from '@immich/ui';
|
||||
import { mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
@ -64,20 +64,9 @@
|
||||
};
|
||||
|
||||
const handleEdit = async (dto: UserAdminResponseDto) => {
|
||||
const result = await modalManager.show(UserEditModal, { user: dto, canResetPassword: dto.id !== $user.id });
|
||||
switch (result?.action) {
|
||||
case 'resetPassword': {
|
||||
await modalManager.show(PasswordResetSuccess, { newPassword: result.data });
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
await refresh();
|
||||
break;
|
||||
}
|
||||
case 'resetPinCode': {
|
||||
notificationController.show({ type: NotificationType.Info, message: $t('pin_code_reset_successfully') });
|
||||
break;
|
||||
}
|
||||
const result = await modalManager.show(UserEditModal, { user: dto });
|
||||
if (result) {
|
||||
await refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@ -114,16 +103,14 @@
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#if allUsers}
|
||||
{#each allUsers as immichUser, index (immichUser.id)}
|
||||
{#each allUsers as immichUser (immichUser.id)}
|
||||
<tr
|
||||
class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {immichUser.deletedAt
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: index % 2 == 0
|
||||
? 'bg-subtle'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'}"
|
||||
: 'even:bg-subtle/20 odd:bg-subtle/80'}"
|
||||
>
|
||||
<td class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-ellipsis break-all px-2 text-sm"
|
||||
>{immichUser.email}</td
|
||||
><Link href="{AppRoute.ADMIN_USERS}/{immichUser.id}">{immichUser.email}</Link></td
|
||||
>
|
||||
<td class="hidden sm:block w-3/12 text-ellipsis break-all px-2 text-sm">{immichUser.name}</td>
|
||||
<td class="hidden xl:block w-3/12 2xl:w-2/12 text-ellipsis break-all px-2 text-sm">
|
18
web/src/routes/admin/users/+page.ts
Normal file
18
web/src/routes/admin/users/+page.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate({ admin: true });
|
||||
await requestServerInfo();
|
||||
const allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
allUsers,
|
||||
meta: {
|
||||
title: $t('admin.user_management'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
343
web/src/routes/admin/users/[id]/+page.svelte
Normal file
343
web/src/routes/admin/users/[id]/+page.svelte
Normal file
@ -0,0 +1,343 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import StatsCard from '$lib/components/admin-page/server-stats/stats-card.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
|
||||
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { user as authUser } from '$lib/stores/user.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateUserAdmin } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Code,
|
||||
Container,
|
||||
Field,
|
||||
getByteUnitString,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
} from '@immich/ui';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiCameraIris,
|
||||
mdiChartPie,
|
||||
mdiChartPieOutline,
|
||||
mdiCheckCircle,
|
||||
mdiFeatureSearchOutline,
|
||||
mdiLockSmart,
|
||||
mdiOnepassword,
|
||||
mdiPencilOutline,
|
||||
mdiPlayCircle,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let user = $derived(data.user);
|
||||
const userPreferences = $derived(data.userPreferences);
|
||||
const userStatistics = $derived(data.userStatistics);
|
||||
|
||||
const TiB = 1024 ** 4;
|
||||
const usage = $derived(user.quotaUsageInBytes ?? 0);
|
||||
let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(usage, usage > TiB ? 2 : 0));
|
||||
|
||||
const usedBytes = $derived(user.quotaUsageInBytes ?? 0);
|
||||
const availableBytes = $derived(user.quotaSizeInBytes ?? 1);
|
||||
let usedPercentage = $derived(Math.min(Math.round((usedBytes / availableBytes) * 100), 100));
|
||||
let canResetPassword = $derived($authUser.id !== user.id);
|
||||
let newPassword = $state<string>('');
|
||||
|
||||
const handleEdit = async () => {
|
||||
const result = await modalManager.show(UserEditModal, { user: { ...user } });
|
||||
if (result) {
|
||||
user = result;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const result = await modalManager.show(UserDeleteConfirmModal, { user });
|
||||
if (result) {
|
||||
await goto(AppRoute.ADMIN_USERS);
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageClass = () => {
|
||||
if (usedPercentage >= 95) {
|
||||
return 'bg-red-500';
|
||||
}
|
||||
|
||||
if (usedPercentage > 80) {
|
||||
return 'bg-yellow-500';
|
||||
}
|
||||
|
||||
return 'bg-immich-primary dark:bg-immich-dark-primary';
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_user_password_reset', { values: { user: user.name } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newPassword = generatePassword();
|
||||
|
||||
await updateUserAdmin({
|
||||
id: user.id,
|
||||
userAdminUpdateDto: {
|
||||
password: newPassword,
|
||||
shouldChangePassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
await modalManager.show(PasswordResetSuccessModal, { newPassword });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_password'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetUserPinCode = async () => {
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } }),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
||||
|
||||
notificationController.show({ type: NotificationType.Info, message: $t('pin_code_reset_successfully') });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO move password reset server-side
|
||||
function generatePassword(length: number = 16) {
|
||||
let generatedPassword = '';
|
||||
|
||||
const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
randomNumber = randomNumber / 2 ** 32;
|
||||
randomNumber = Math.floor(randomNumber * characterSet.length);
|
||||
|
||||
generatedPassword += characterSet[randomNumber];
|
||||
}
|
||||
|
||||
return generatedPassword;
|
||||
}
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title} admin>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
{#if canResetPassword}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
leadingIcon={mdiOnepassword}
|
||||
onclick={handleResetPassword}
|
||||
>
|
||||
<Text class="hidden md:block">{$t('reset_password')}</Text>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
leadingIcon={mdiLockSmart}
|
||||
onclick={handleResetUserPinCode}
|
||||
>
|
||||
<Text class="hidden md:block">{$t('reset_pin_code')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
leadingIcon={mdiPencilOutline}
|
||||
onclick={() => handleEdit()}
|
||||
>
|
||||
<Text class="hidden md:block">{$t('edit_user')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
leadingIcon={mdiTrashCanOutline}
|
||||
onclick={() => handleDelete()}
|
||||
>
|
||||
<Text class="hidden md:block">{$t('delete_user')}</Text>
|
||||
</Button>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Container size="large" center>
|
||||
<div class="grid gap-4 grod-cols-1 lg:grid-cols-2 w-full">
|
||||
<div class="col-span-full flex gap-4 items-center my-4">
|
||||
<UserAvatar {user} size="md" />
|
||||
<Heading tag="h1" size="large">{user.name}</Heading>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<div class="flex flex-col lg:flex-row gap-4 w-full">
|
||||
<StatsCard icon={mdiCameraIris} title={$t('photos').toUpperCase()} value={userStatistics.images} />
|
||||
<StatsCard icon={mdiPlayCircle} title={$t('videos').toUpperCase()} value={userStatistics.videos} />
|
||||
<StatsCard
|
||||
icon={mdiChartPie}
|
||||
title={$t('storage').toUpperCase()}
|
||||
value={statsUsage}
|
||||
unit={statsUsageUnit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Card color="secondary">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={mdiAccountOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('profile')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Stack gap={2}>
|
||||
<div>
|
||||
<Heading tag="h3" size="tiny">{$t('name')}</Heading>
|
||||
<Text>{user.name}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h3" size="tiny">{$t('email')}</Heading>
|
||||
<Text>{user.email}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h3" size="tiny">{$t('created_at')}</Heading>
|
||||
<Text>{user.createdAt}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h3" size="tiny">{$t('updated_at')}</Heading>
|
||||
<Text>{user.updatedAt}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h3" size="tiny">{$t('id')}</Heading>
|
||||
<Code>{user.id}</Code>
|
||||
</div>
|
||||
</Stack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<Card color="secondary">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={mdiFeatureSearchOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('features')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div>
|
||||
<Stack gap={2}>
|
||||
<Field readOnly label={$t('email_notifications')}>
|
||||
<Switch checked={userPreferences.emailNotifications.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('folders')}>
|
||||
<Switch checked={userPreferences.folders.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('memories')}>
|
||||
<Switch checked={userPreferences.memories.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('people')}>
|
||||
<Switch checked={userPreferences.people.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('rating')}>
|
||||
<Switch checked={userPreferences.ratings.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('shared_links')}>
|
||||
<Switch checked={userPreferences.sharedLinks.enabled} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('show_supporter_badge')}>
|
||||
<Switch checked={userPreferences.purchase.showSupportBadge} color="primary" />
|
||||
</Field>
|
||||
<Field readOnly label={$t('tags')}>
|
||||
<Switch checked={userPreferences.tags.enabled} color="primary" />
|
||||
</Field>
|
||||
</Stack>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card color="secondary">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={mdiChartPieOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('storage_quota')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div>
|
||||
{#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
|
||||
<Text>
|
||||
{$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale, 3),
|
||||
available: getByteUnitString(availableBytes, $locale, 3),
|
||||
},
|
||||
})}
|
||||
</Text>
|
||||
{:else}
|
||||
<Text class="flex items-center gap-1">
|
||||
<Icon icon={mdiCheckCircle} size="1.25rem" class="text-success" />
|
||||
{$t('unlimited')}
|
||||
</Text>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
|
||||
<div
|
||||
class="storage-status p-4 mt-4 bg-gray-100 dark:bg-immich-dark-primary/10 rounded-lg text-sm w-full"
|
||||
title={$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale, 3),
|
||||
available: getByteUnitString(availableBytes, $locale, 3),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
|
||||
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {getUsageClass()}" style="width: {usedPercentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</UserPageLayout>
|
31
web/src/routes/admin/users/[id]/+page.ts
Normal file
31
web/src/routes/admin/users/[id]/+page.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getUserPreferencesAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
await authenticate({ admin: true });
|
||||
await requestServerInfo();
|
||||
const [user] = await searchUsersAdmin({ id: params.id }).catch(() => []);
|
||||
if (!user) {
|
||||
redirect(302, AppRoute.ADMIN_USERS);
|
||||
}
|
||||
|
||||
const [userPreferences, userStatistics] = await Promise.all([
|
||||
getUserPreferencesAdmin({ id: user.id }),
|
||||
getUserStatisticsAdmin({ id: user.id }),
|
||||
]);
|
||||
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
user,
|
||||
userPreferences,
|
||||
userStatistics,
|
||||
meta: {
|
||||
title: $t('admin.user_details'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
Loading…
x
Reference in New Issue
Block a user