1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web): favorite an asset (#939)

* feat(web): favorite an asset

* fix: test and linting

* fix: asset dto type
This commit is contained in:
Jason Rasmussen 2022-11-08 11:20:36 -05:00 committed by GitHub
parent 8a9b0347bb
commit 99da181cfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 182 additions and 12 deletions

View File

@ -58,6 +58,7 @@ doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md
doc/UsageByUserDto.md
@ -132,6 +133,7 @@ lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart
lib/model/usage_by_user_dto.dart

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,8 +4,8 @@ import { BadRequestException, NotFoundException, ForbiddenException } from '@nes
import { AlbumEntity } from '@app/database/entities/album.entity';
import { AlbumResponseDto } from './response-dto/album-response.dto';
import { IAssetRepository } from '../asset/asset-repository';
import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto";
import {IAlbumRepository} from "./album-repository";
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
import { IAlbumRepository } from './album-repository';
describe('Album service', () => {
let sut: AlbumService;
@ -125,6 +125,7 @@ describe('Album service', () => {
assetRepositoryMock = {
create: jest.fn(),
update: jest.fn(),
getAllByUserId: jest.fn(),
getAllByDeviceId: jest.fn(),
getAssetCountByTimeBucket: jest.fn(),
@ -333,7 +334,7 @@ describe('Album service', () => {
const albumResponse: AddAssetsResponseDto = {
alreadyInAlbum: [],
successfullyAdded: 1
successfullyAdded: 1,
};
const albumId = albumEntity.id;
@ -341,13 +342,13 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
const result = await sut.addAssetsToAlbum(
const result = (await sut.addAssetsToAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
) as AddAssetsResponseDto;
)) as AddAssetsResponseDto;
// TODO: stub and expect album rendered
expect(result.album?.id).toEqual(albumId);
@ -358,7 +359,7 @@ describe('Album service', () => {
const albumResponse: AddAssetsResponseDto = {
alreadyInAlbum: [],
successfullyAdded: 1
successfullyAdded: 1,
};
const albumId = albumEntity.id;
@ -366,13 +367,13 @@ describe('Album service', () => {
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
const result = await sut.addAssetsToAlbum(
const result = (await sut.addAssetsToAlbum(
authUser,
{
assetIds: ['1'],
},
albumId,
) as AddAssetsResponseDto;
)) as AddAssetsResponseDto;
// TODO: stub and expect album rendered
expect(result.album?.id).toEqual(albumId);
@ -383,7 +384,7 @@ describe('Album service', () => {
const albumResponse: AddAssetsResponseDto = {
alreadyInAlbum: [],
successfullyAdded: 1
successfullyAdded: 1,
};
const albumId = albumEntity.id;
@ -447,7 +448,7 @@ describe('Album service', () => {
const albumResponse: AddAssetsResponseDto = {
alreadyInAlbum: [],
successfullyAdded: 1
successfullyAdded: 1,
};
const albumId = albumEntity.id;

View File

@ -13,6 +13,7 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
import { In } from 'typeorm/find-options/operator/In';
import { UpdateAssetDto } from './dto/update-asset.dto';
export interface IAssetRepository {
create(
@ -22,6 +23,7 @@ export interface IAssetRepository {
mimeType: string,
checksum?: Buffer,
): Promise<AssetEntity>;
update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
getAllByUserId(userId: string): Promise<AssetEntity[]>;
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
getById(assetId: string): Promise<AssetEntity>;
@ -252,6 +254,15 @@ export class AssetRepository implements IAssetRepository {
return createdAsset;
}
/**
* Update asset
*/
async update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
return await this.assetRepository.save(asset);
}
/**
* Get assets by device's Id on the database
* @param userId

View File

@ -15,6 +15,7 @@ import {
BadRequestException,
UploadedFile,
Header,
Put,
} from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service';
@ -50,6 +51,7 @@ import { QueryFailedError } from 'typeorm';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
@Authenticated()
@ApiBearerAuth()
@ -222,6 +224,18 @@ export class AssetController {
return await this.assetService.getAssetById(authUser, assetId);
}
/**
* Update an asset
*/
@Put('/assetById/:assetId')
async updateAssetById(
@GetAuthUser() authUser: AuthUserDto,
@Param('assetId') assetId: string,
@Body() dto: UpdateAssetDto,
): Promise<AssetResponseDto> {
return await this.assetService.updateAssetById(authUser, assetId, dto);
}
@Delete('/')
async deleteAsset(
@GetAuthUser() authUser: AuthUserDto,

View File

@ -97,6 +97,7 @@ describe('AssetService', () => {
beforeAll(() => {
assetRepositoryMock = {
create: jest.fn(),
update: jest.fn(),
getAllByUserId: jest.fn(),
getAllByDeviceId: jest.fn(),
getAssetCountByTimeBucket: jest.fn(),

View File

@ -1,6 +1,7 @@
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import {
BadRequestException,
ForbiddenException,
Inject,
Injectable,
InternalServerErrorException,
@ -39,6 +40,7 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
import { timeUtils } from '@app/common/utils';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
const fileInfo = promisify(stat);
@ -123,6 +125,21 @@ export class AssetService {
return mapAsset(asset);
}
public async updateAssetById(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
const asset = await this._assetRepository.getById(assetId);
if (!asset) {
throw new BadRequestException('Asset not found');
}
if (authUser.id !== asset.userId) {
throw new ForbiddenException('Not the owner');
}
const updatedAsset = await this._assetRepository.update(asset, dto);
return mapAsset(updatedAsset);
}
public async downloadFile(query: ServeFileDto, res: Res) {
try {
let fileReadStream = null;

View File

@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class UpdateAssetDto {
@IsBoolean()
isFavorite!: boolean;
}

File diff suppressed because one or more lines are too long

View File

@ -1391,6 +1391,19 @@ export interface UpdateAlbumDto {
*/
'albumThumbnailAssetId'?: string;
}
/**
*
* @export
* @interface UpdateAssetDto
*/
export interface UpdateAssetDto {
/**
*
* @type {boolean}
* @memberof UpdateAssetDto
*/
'isFavorite': boolean;
}
/**
*
* @export
@ -3058,6 +3071,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
* Update an asset
* @summary
* @param {string} assetId
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetById: async (assetId: string, updateAssetDto: UpdateAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetId' is not null or undefined
assertParamExists('updateAssetById', 'assetId', assetId)
// verify required parameter 'updateAssetDto' is not null or undefined
assertParamExists('updateAssetById', 'updateAssetDto', updateAssetDto)
const localVarPath = `/asset/assetById/{assetId}`
.replace(`{${"assetId"}}`, encodeURIComponent(String(assetId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateAssetDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {any} assetData
@ -3279,6 +3336,18 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.serveFile(aid, did, isThumb, isWeb, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Update an asset
* @summary
* @param {string} assetId
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetById(assetId, updateAssetDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {any} assetData
@ -3450,6 +3519,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
serveFile(aid: string, did: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
return localVarFp.serveFile(aid, did, isThumb, isWeb, options).then((request) => request(axios, basePath));
},
/**
* Update an asset
* @summary
* @param {string} assetId
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
return localVarFp.updateAssetById(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {any} assetData
@ -3652,6 +3732,19 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).serveFile(aid, did, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
}
/**
* Update an asset
* @summary
* @param {string} assetId
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public updateAssetById(assetId: string, updateAssetDto: UpdateAssetDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).updateAssetById(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {any} assetData

View File

@ -9,6 +9,14 @@
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import Star from 'svelte-material-icons/Star.svelte';
import StarOutline from 'svelte-material-icons/StarOutline.svelte';
import { page } from '$app/stores';
import { AssetResponseDto } from '../../../api';
export let asset: AssetResponseDto;
const isOwner = asset.ownerId === $page.data.user.id;
const dispatch = createEventDispatcher();
@ -38,8 +46,15 @@
</div>
<div class="text-white flex gap-2">
<CircleIconButton logo={CloudDownloadOutline} on:click={() => dispatch('download')} />
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} />
<CircleIconButton logo={InformationOutline} on:click={() => dispatch('showDetail')} />
{#if isOwner}
<CircleIconButton
logo={asset.isFavorite ? Star : StarOutline}
on:click={() => dispatch('favorite')}
title="Favorite"
/>
{/if}
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} />
<CircleIconButton logo={DotsVertical} on:click={(event) => showOptionsMenu(event)} />
</div>
</div>

View File

@ -178,6 +178,14 @@
}
};
const toggleFavorite = async () => {
const { data } = await api.assetApi.updateAssetById(asset.id, {
isFavorite: !asset.isFavorite
});
asset.isFavorite = data.isFavorite;
};
const openAlbumPicker = (shared: boolean) => {
isShowAlbumPicker = true;
addToSharedAlbum = shared;
@ -218,10 +226,12 @@
>
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
<AsserViewerNavBar
{asset}
on:goBack={closeViewer}
on:showDetail={showDetailInfoHandler}
on:download={downloadFile}
on:delete={deleteAsset}
on:favorite={toggleFavorite}
on:addToAlbum={() => openAlbumPicker(false)}
on:addToSharedAlbum={() => openAlbumPicker(true)}
/>