mirror of
https://github.com/immich-app/immich.git
synced 2024-11-16 02:18:50 -07:00
Remove/Add asset in ablum on web (#371)
* Added interaction to select multiple thumbnail * Fixed stutter transition * Return AlbumResponseDto after removing an asset from album * Render correctly when an array of thumbnail is updated * Fixed wording * Added native dialog for removing users from album * Fixed rendering incorrect profile image on share user select dialog
This commit is contained in:
parent
a35460cb84
commit
052db5d748
@ -101,4 +101,3 @@ lib/model/user_count_response_dto.dart
|
|||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
pubspec.yaml
|
pubspec.yaml
|
||||||
test/logout_response_dto_test.dart
|
|
||||||
|
@ -306,7 +306,7 @@ Name | Type | Description | Notes
|
|||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **removeAssetFromAlbum**
|
# **removeAssetFromAlbum**
|
||||||
> removeAssetFromAlbum(albumId, removeAssetsDto)
|
> AlbumResponseDto removeAssetFromAlbum(albumId, removeAssetsDto)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -325,7 +325,8 @@ final albumId = albumId_example; // String |
|
|||||||
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
|
final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
api_instance.removeAssetFromAlbum(albumId, removeAssetsDto);
|
final result = api_instance.removeAssetFromAlbum(albumId, removeAssetsDto);
|
||||||
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n');
|
print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n');
|
||||||
}
|
}
|
||||||
@ -340,7 +341,7 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
void (empty response body)
|
[**AlbumResponseDto**](AlbumResponseDto.md)
|
||||||
|
|
||||||
### Authorization
|
### Authorization
|
||||||
|
|
||||||
@ -349,7 +350,7 @@ void (empty response body)
|
|||||||
### HTTP request headers
|
### HTTP request headers
|
||||||
|
|
||||||
- **Content-Type**: application/json
|
- **Content-Type**: application/json
|
||||||
- **Accept**: Not defined
|
- **Accept**: application/json
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
@ -346,11 +346,19 @@ class AlbumApi {
|
|||||||
/// * [String] albumId (required):
|
/// * [String] albumId (required):
|
||||||
///
|
///
|
||||||
/// * [RemoveAssetsDto] removeAssetsDto (required):
|
/// * [RemoveAssetsDto] removeAssetsDto (required):
|
||||||
Future<void> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto,) async {
|
Future<AlbumResponseDto?> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto,) async {
|
||||||
final response = await removeAssetFromAlbumWithHttpInfo(albumId, removeAssetsDto,);
|
final response = await removeAssetFromAlbumWithHttpInfo(albumId, removeAssetsDto,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
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), 'AlbumResponseDto',) as AlbumResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response].
|
/// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response].
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumEntity } from '@app/database/entities/album.entity';
|
||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, SelectQueryBuilder, DataSource } from 'typeorm';
|
import { Repository, SelectQueryBuilder, DataSource } from 'typeorm';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
@ -10,6 +10,7 @@ import { CreateAlbumDto } from './dto/create-album.dto';
|
|||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository {
|
||||||
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
||||||
@ -18,7 +19,7 @@ export interface IAlbumRepository {
|
|||||||
delete(album: AlbumEntity): Promise<void>;
|
delete(album: AlbumEntity): Promise<void>;
|
||||||
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
||||||
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
||||||
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<boolean>;
|
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>;
|
||||||
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
|
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
|
||||||
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
|
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
|
||||||
}
|
}
|
||||||
@ -198,7 +199,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
|
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<boolean> {
|
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<AlbumEntity> {
|
||||||
let deleteAssetCount = 0;
|
let deleteAssetCount = 0;
|
||||||
// TODO: should probably do a single delete query?
|
// TODO: should probably do a single delete query?
|
||||||
for (const assetId of removeAssetsDto.assetIds) {
|
for (const assetId of removeAssetsDto.assetIds) {
|
||||||
@ -207,7 +208,11 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: No need to return boolean if using a singe delete query
|
// TODO: No need to return boolean if using a singe delete query
|
||||||
return deleteAssetCount == removeAssetsDto.assetIds.length;
|
if (deleteAssetCount == removeAssetsDto.assetIds.length) {
|
||||||
|
return this.get(album.id) as Promise<AlbumEntity>;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException('Some assets were not found in the album');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity> {
|
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity> {
|
||||||
|
@ -23,6 +23,7 @@ import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
|||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
|
||||||
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ -76,7 +77,7 @@ export class AlbumController {
|
|||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto,
|
@Body(ValidationPipe) removeAssetsDto: RemoveAssetsDto,
|
||||||
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
|
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
|
||||||
) {
|
): Promise<AlbumResponseDto> {
|
||||||
return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
|
return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,9 +82,15 @@ export class AlbumService {
|
|||||||
|
|
||||||
// async removeUsersFromAlbum() {}
|
// async removeUsersFromAlbum() {}
|
||||||
|
|
||||||
async removeAssetsFromAlbum(authUser: AuthUserDto, removeAssetsDto: RemoveAssetsDto, albumId: string): Promise<void> {
|
async removeAssetsFromAlbum(
|
||||||
|
authUser: AuthUserDto,
|
||||||
|
removeAssetsDto: RemoveAssetsDto,
|
||||||
|
albumId: string,
|
||||||
|
): Promise<AlbumResponseDto> {
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
const album = await this._getAlbum({ authUser, albumId });
|
||||||
await this._albumRepository.removeAssets(album, removeAssetsDto);
|
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
|
||||||
|
|
||||||
|
return mapAlbum(updateAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAssetsToAlbum(
|
async addAssetsToAlbum(
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1608,7 +1608,7 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async removeAssetFromAlbum(albumId: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
async removeAssetFromAlbum(albumId: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(albumId, removeAssetsDto, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(albumId, removeAssetsDto, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
@ -1707,7 +1707,7 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeAssetFromAlbum(albumId: string, removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<void> {
|
removeAssetFromAlbum(albumId: string, removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<AlbumResponseDto> {
|
||||||
return localVarFp.removeAssetFromAlbum(albumId, removeAssetsDto, options).then((request) => request(axios, basePath));
|
return localVarFp.removeAssetFromAlbum(albumId, removeAssetsDto, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
import Close from 'svelte-material-icons/Close.svelte';
|
import Close from 'svelte-material-icons/Close.svelte';
|
||||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
export let backIcon = Close;
|
export let backIcon = Close;
|
||||||
let appBarBorder = 'bg-immich-bg';
|
export let tailwindClasses = '';
|
||||||
|
|
||||||
|
let appBarBorder = 'bg-immich-bg border border-transparent';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -15,7 +18,7 @@
|
|||||||
if (window.pageYOffset > 80) {
|
if (window.pageYOffset > 80) {
|
||||||
appBarBorder = 'border border-gray-200 bg-gray-50';
|
appBarBorder = 'border border-gray-200 bg-gray-50';
|
||||||
} else {
|
} else {
|
||||||
appBarBorder = 'bg-immich-bg';
|
appBarBorder = 'bg-immich-bg border border-transparent';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -28,10 +31,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed top-0 w-full bg-transparent z-[100]">
|
<div
|
||||||
|
transition:fly|local={{ y: 10, duration: 200 }}
|
||||||
|
class="fixed top-0 w-full bg-transparent z-[100]"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
id="asset-selection-app-bar"
|
id="asset-selection-app-bar"
|
||||||
class={`flex justify-between ${appBarBorder} rounded-lg p-2 mx-2 mt-2 transition-all place-items-center`}
|
class={`flex justify-between ${appBarBorder} rounded-lg p-2 mx-2 mt-2 transition-all place-items-center ${tailwindClasses}`}
|
||||||
>
|
>
|
||||||
<div class="flex place-items-center gap-6">
|
<div class="flex place-items-center gap-6">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { afterNavigate, goto } from '$app/navigation';
|
import { afterNavigate, goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { AlbumResponseDto, api, AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
|
import { AlbumResponseDto, api, AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||||
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
|
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
|
||||||
@ -16,8 +16,8 @@
|
|||||||
import UserSelectionModal from './user-selection-modal.svelte';
|
import UserSelectionModal from './user-selection-modal.svelte';
|
||||||
import ShareInfoModal from './share-info-modal.svelte';
|
import ShareInfoModal from './share-info-modal.svelte';
|
||||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||||
|
import Close from 'svelte-material-icons/Close.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
|
|
||||||
let isShowAssetViewer = false;
|
let isShowAssetViewer = false;
|
||||||
@ -39,6 +39,9 @@
|
|||||||
|
|
||||||
$: isOwned = currentUser?.id == album.ownerId;
|
$: isOwned = currentUser?.id == album.ownerId;
|
||||||
|
|
||||||
|
let multiSelectAsset: Set<AssetResponseDto> = new Set();
|
||||||
|
$: isMultiSelectionMode = multiSelectAsset.size > 0;
|
||||||
|
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
backUrl = from?.pathname ?? '/albums';
|
backUrl = from?.pathname ?? '/albums';
|
||||||
|
|
||||||
@ -81,15 +84,46 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const viewAsset = (event: CustomEvent) => {
|
const viewAssetHandler = (event: CustomEvent) => {
|
||||||
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
|
const { asset }: { asset: AssetResponseDto } = event.detail;
|
||||||
|
|
||||||
currentViewAssetIndex = album.assets.findIndex((a) => a.id == assetId);
|
currentViewAssetIndex = album.assets.findIndex((a) => a.id == asset.id);
|
||||||
selectedAsset = album.assets[currentViewAssetIndex];
|
selectedAsset = album.assets[currentViewAssetIndex];
|
||||||
isShowAssetViewer = true;
|
isShowAssetViewer = true;
|
||||||
pushState(selectedAsset.id);
|
pushState(selectedAsset.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectAssetHandler = (event: CustomEvent) => {
|
||||||
|
const { asset }: { asset: AssetResponseDto } = event.detail;
|
||||||
|
let temp = new Set(multiSelectAsset);
|
||||||
|
|
||||||
|
if (multiSelectAsset.has(asset)) {
|
||||||
|
temp.delete(asset);
|
||||||
|
} else {
|
||||||
|
temp.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiSelectAsset = temp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearMultiSelectAssetAssetHandler = () => {
|
||||||
|
multiSelectAsset = new Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedAssetFromAlbum = async () => {
|
||||||
|
if (window.confirm('Do you want to remove selected assets from the album?')) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.albumApi.removeAssetFromAlbum(album.id, {
|
||||||
|
assetIds: Array.from(multiSelectAsset).map((a) => a.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
album = data;
|
||||||
|
multiSelectAsset = new Set();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error [album-viewer] [removeAssetFromAlbum]', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const navigateAssetForward = () => {
|
const navigateAssetForward = () => {
|
||||||
try {
|
try {
|
||||||
if (currentViewAssetIndex < album.assets.length - 1) {
|
if (currentViewAssetIndex < album.assets.length - 1) {
|
||||||
@ -191,32 +225,60 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="bg-immich-bg">
|
<section class="bg-immich-bg">
|
||||||
<AlbumAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
<!-- Multiselection mode app bar -->
|
||||||
<svelte:fragment slot="trailing">
|
{#if isMultiSelectionMode}
|
||||||
{#if album.assets.length > 0}
|
<AlbumAppBar
|
||||||
<CircleIconButton
|
on:close-button-click={clearMultiSelectAssetAssetHandler}
|
||||||
title="Add Photos"
|
backIcon={Close}
|
||||||
on:click={() => (isShowAssetSelection = true)}
|
tailwindClasses={'bg-white shadow-md'}
|
||||||
logo={FileImagePlusOutline}
|
>
|
||||||
/>
|
<svelte:fragment slot="leading">
|
||||||
|
<p class="font-medium text-immich-primary">Selected {multiSelectAsset.size}</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="trailing">
|
||||||
|
{#if isOwned}
|
||||||
|
<CircleIconButton
|
||||||
|
title="Remove from album"
|
||||||
|
on:click={removeSelectedAssetFromAlbum}
|
||||||
|
logo={DeleteOutline}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</AlbumAppBar>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<CircleIconButton
|
<!-- Default app bar -->
|
||||||
title="Share"
|
{#if !isMultiSelectionMode}
|
||||||
on:click={() => (isShowShareUserSelection = true)}
|
<AlbumAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
||||||
logo={ShareVariantOutline}
|
<svelte:fragment slot="trailing">
|
||||||
/>
|
{#if album.assets.length > 0}
|
||||||
{/if}
|
<CircleIconButton
|
||||||
|
title="Add Photos"
|
||||||
|
on:click={() => (isShowAssetSelection = true)}
|
||||||
|
logo={FileImagePlusOutline}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
|
<!-- Sharing only for owner -->
|
||||||
<button
|
{#if isOwned}
|
||||||
disabled={album.assets.length == 0}
|
<CircleIconButton
|
||||||
on:click={() => (isShowShareUserSelection = true)}
|
title="Share"
|
||||||
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
on:click={() => (isShowShareUserSelection = true)}
|
||||||
><span class="px-2">Share</span></button
|
logo={ShareVariantOutline}
|
||||||
>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
{/if}
|
||||||
</AlbumAppBar>
|
|
||||||
|
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
|
||||||
|
<button
|
||||||
|
disabled={album.assets.length == 0}
|
||||||
|
on:click={() => (isShowShareUserSelection = true)}
|
||||||
|
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
||||||
|
><span class="px-2">Share</span></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</AlbumAppBar>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<section class="m-auto my-[160px] w-[60%]">
|
<section class="m-auto my-[160px] w-[60%]">
|
||||||
<input
|
<input
|
||||||
@ -237,9 +299,11 @@
|
|||||||
{#if album.shared}
|
{#if album.shared}
|
||||||
<div class="my-6 flex">
|
<div class="my-6 flex">
|
||||||
{#each album.sharedUsers as user}
|
{#each album.sharedUsers as user}
|
||||||
<span class="mr-1">
|
{#key user.id}
|
||||||
<CircleAvatar {user} on:click={() => (isShowShareInfoModal = true)} />
|
<span class="mr-1">
|
||||||
</span>
|
<CircleAvatar {user} on:click={() => (isShowShareInfoModal = true)} />
|
||||||
|
</span>
|
||||||
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -255,16 +319,28 @@
|
|||||||
{#if album.assets.length > 0}
|
{#if album.assets.length > 0}
|
||||||
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
||||||
{#each album.assets as asset}
|
{#each album.assets as asset}
|
||||||
{#if album.assets.length < 7}
|
{#key asset.id}
|
||||||
<ImmichThumbnail
|
{#if album.assets.length < 7}
|
||||||
{asset}
|
<ImmichThumbnail
|
||||||
{thumbnailSize}
|
{asset}
|
||||||
format={ThumbnailFormat.Jpeg}
|
{thumbnailSize}
|
||||||
on:click={viewAsset}
|
format={ThumbnailFormat.Jpeg}
|
||||||
/>
|
on:click={(e) =>
|
||||||
{:else}
|
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
|
||||||
<ImmichThumbnail {asset} {thumbnailSize} on:click={viewAsset} />
|
on:select={selectAssetHandler}
|
||||||
{/if}
|
selected={multiSelectAsset.has(asset)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ImmichThumbnail
|
||||||
|
{asset}
|
||||||
|
{thumbnailSize}
|
||||||
|
on:click={(e) =>
|
||||||
|
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
|
||||||
|
on:select={selectAssetHandler}
|
||||||
|
selected={multiSelectAsset.has(asset)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -42,11 +42,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeUser = async (userId: string) => {
|
const removeUser = async (userId: string) => {
|
||||||
try {
|
if (window.confirm('Do you want to remove selected user from the album?')) {
|
||||||
await api.albumApi.removeUserFromAlbum(album.id, userId);
|
try {
|
||||||
dispatch('user-deleted', { userId });
|
await api.albumApi.removeUserFromAlbum(album.id, userId);
|
||||||
} catch (e) {
|
dispatch('user-deleted', { userId });
|
||||||
console.error('Error [share-info-modal] [removeUser]', e);
|
} catch (e) {
|
||||||
|
console.error('Error [share-info-modal] [removeUser]', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
export let sharedUsersInAlbum: Set<UserResponseDto>;
|
export let sharedUsersInAlbum: Set<UserResponseDto>;
|
||||||
let users: UserResponseDto[] = [];
|
let users: UserResponseDto[] = [];
|
||||||
let selectedUsers: Set<UserResponseDto> = new Set();
|
let selectedUsers: UserResponseDto[] = [];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@ -22,23 +22,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectUser = (user: UserResponseDto) => {
|
const selectUser = (user: UserResponseDto) => {
|
||||||
const tempSelectedUsers = new Set(selectedUsers);
|
if (selectedUsers.includes(user)) {
|
||||||
|
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||||
if (selectedUsers.has(user)) {
|
|
||||||
tempSelectedUsers.delete(user);
|
|
||||||
} else {
|
} else {
|
||||||
tempSelectedUsers.add(user);
|
selectedUsers = [...selectedUsers, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedUsers = tempSelectedUsers;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deselectUser = (user: UserResponseDto) => {
|
const deselectUser = (user: UserResponseDto) => {
|
||||||
const tempSelectedUsers = new Set(selectedUsers);
|
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||||
|
|
||||||
tempSelectedUsers.delete(user);
|
|
||||||
|
|
||||||
selectedUsers = tempSelectedUsers;
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -51,18 +43,20 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<div class=" max-h-[400px] overflow-y-auto immich-scrollbar">
|
<div class=" max-h-[400px] overflow-y-auto immich-scrollbar">
|
||||||
{#if selectedUsers.size > 0}
|
{#if selectedUsers.length > 0}
|
||||||
<div class="flex gap-4 py-2 px-5 overflow-x-auto place-items-center mb-2">
|
<div class="flex gap-4 py-2 px-5 overflow-x-auto place-items-center mb-2">
|
||||||
<p class="font-medium">To</p>
|
<p class="font-medium">To</p>
|
||||||
|
|
||||||
{#each Array.from(selectedUsers) as user}
|
{#each selectedUsers as user}
|
||||||
<button
|
{#key user.id}
|
||||||
on:click={() => deselectUser(user)}
|
<button
|
||||||
class="flex gap-1 place-items-center border border-gray-400 rounded-full p-1 hover:bg-gray-200 transition-colors"
|
on:click={() => deselectUser(user)}
|
||||||
>
|
class="flex gap-1 place-items-center border border-gray-400 rounded-full p-1 hover:bg-gray-200 transition-colors"
|
||||||
<CircleAvatar size={28} {user} />
|
>
|
||||||
<p class="text-xs font-medium">{user.firstName} {user.lastName}</p>
|
<CircleAvatar size={28} {user} />
|
||||||
</button>
|
<p class="text-xs font-medium">{user.firstName} {user.lastName}</p>
|
||||||
|
</button>
|
||||||
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -76,7 +70,7 @@
|
|||||||
on:click={() => selectUser(user)}
|
on:click={() => selectUser(user)}
|
||||||
class="w-full flex place-items-center gap-4 py-4 px-5 hover:bg-gray-200 transition-all"
|
class="w-full flex place-items-center gap-4 py-4 px-5 hover:bg-gray-200 transition-all"
|
||||||
>
|
>
|
||||||
{#if selectedUsers.has(user)}
|
{#if selectedUsers.includes(user)}
|
||||||
<span
|
<span
|
||||||
class="bg-immich-primary text-white rounded-full w-12 h-12 border flex place-items-center place-content-center text-3xl"
|
class="bg-immich-primary text-white rounded-full w-12 h-12 border flex place-items-center place-content-center text-3xl"
|
||||||
>✓</span
|
>✓</span
|
||||||
@ -104,7 +98,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if selectedUsers.size > 0}
|
{#if selectedUsers.length > 0}
|
||||||
<div class="flex place-content-end p-5 ">
|
<div class="flex place-content-end p-5 ">
|
||||||
<button
|
<button
|
||||||
on:click={() => dispatch('add-user', { selectedUsers })}
|
on:click={() => dispatch('add-user', { selectedUsers })}
|
||||||
|
@ -171,9 +171,14 @@
|
|||||||
};
|
};
|
||||||
const thumbnailClickedHandler = () => {
|
const thumbnailClickedHandler = () => {
|
||||||
if (!isExisted) {
|
if (!isExisted) {
|
||||||
dispatch('click', { assetId: asset.id, deviceId: asset.deviceId });
|
dispatch('click', { asset });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onIconClickedHandler = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch('select', { asset });
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={true} let:intersecting>
|
<IntersectionObserver once={true} let:intersecting>
|
||||||
@ -192,7 +197,8 @@
|
|||||||
in:fade={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
|
class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
|
on:click={onIconClickedHandler}
|
||||||
on:mouseenter={() => (mouseOverIcon = true)}
|
on:mouseenter={() => (mouseOverIcon = true)}
|
||||||
on:mouseleave={() => (mouseOverIcon = false)}
|
on:mouseleave={() => (mouseOverIcon = false)}
|
||||||
class="inline-block"
|
class="inline-block"
|
||||||
@ -204,7 +210,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<CheckCircle size="24" color={mouseOverIcon ? 'white' : '#d8dadb'} />
|
<CheckCircle size="24" color={mouseOverIcon ? 'white' : '#d8dadb'} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -58,9 +58,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const viewAssetHandler = (event: CustomEvent) => {
|
const viewAssetHandler = (event: CustomEvent) => {
|
||||||
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
|
const { asset }: { asset: AssetResponseDto } = event.detail;
|
||||||
|
|
||||||
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == asset.id);
|
||||||
selectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
selectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
||||||
isShowAssetViewer = true;
|
isShowAssetViewer = true;
|
||||||
pushState(selectedAsset.id);
|
pushState(selectedAsset.id);
|
||||||
@ -170,12 +170,14 @@
|
|||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div class="flex flex-wrap gap-[2px]">
|
<div class="flex flex-wrap gap-[2px]">
|
||||||
{#each assetsInDateGroup as asset}
|
{#each assetsInDateGroup as asset}
|
||||||
<ImmichThumbnail
|
{#key asset.id}
|
||||||
{asset}
|
<ImmichThumbnail
|
||||||
on:mouseEvent={thumbnailMouseEventHandler}
|
{asset}
|
||||||
on:click={viewAssetHandler}
|
on:mouseEvent={thumbnailMouseEventHandler}
|
||||||
{groupIndex}
|
on:click={viewAssetHandler}
|
||||||
/>
|
{groupIndex}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user