mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(server, web): include pictures of shared albums on map (#7439)
* feat(server, web): include pictures of shared albums on map * run prettier * re-create api clients * implement suggestions from code review * shared from partner -> shared from partners * rename to 'include shared partner assets' * chore: fix tsc error in server and prettier in web * fix: include assets shared via owner albums --------- Co-authored-by: Zack Pollard <zackpollard@ymail.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
d121903b38
commit
48927f5fb9
13 changed files with 66 additions and 18 deletions
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
|
@ -1386,6 +1386,14 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withSharedAlbums",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
|
@ -1442,12 +1442,13 @@ export function runAssetJobs({ assetJobsDto }: {
|
||||||
body: assetJobsDto
|
body: assetJobsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners }: {
|
export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, isFavorite, withPartners, withSharedAlbums }: {
|
||||||
fileCreatedAfter?: string;
|
fileCreatedAfter?: string;
|
||||||
fileCreatedBefore?: string;
|
fileCreatedBefore?: string;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
withPartners?: boolean;
|
withPartners?: boolean;
|
||||||
|
withSharedAlbums?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -1457,7 +1458,8 @@ export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived,
|
||||||
fileCreatedBefore,
|
fileCreatedBefore,
|
||||||
isArchived,
|
isArchived,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
withPartners
|
withPartners,
|
||||||
|
withSharedAlbums
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -317,6 +317,9 @@ export class MapMarkerDto {
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
withPartners?: boolean;
|
withPartners?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
withSharedAlbums?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemoryLaneDto {
|
export class MemoryLaneDto {
|
||||||
|
|
|
@ -183,7 +183,7 @@ export interface IAssetRepository {
|
||||||
softDeleteAll(ids: string[]): Promise<void>;
|
softDeleteAll(ids: string[]): Promise<void>;
|
||||||
restoreAll(ids: string[]): Promise<void>;
|
restoreAll(ids: string[]): Promise<void>;
|
||||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||||
getMapMarkers(ownerIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||||
|
|
|
@ -490,9 +490,24 @@ export class AssetRepository implements IAssetRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
async getMapMarkers(
|
||||||
|
ownerIds: string[],
|
||||||
|
albumIds: string[],
|
||||||
|
options: MapMarkerSearchOptions = {},
|
||||||
|
): Promise<MapMarker[]> {
|
||||||
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
isVisible: true,
|
||||||
|
isArchived,
|
||||||
|
exifInfo: {
|
||||||
|
latitude: Not(IsNull()),
|
||||||
|
longitude: Not(IsNull()),
|
||||||
|
},
|
||||||
|
isFavorite,
|
||||||
|
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
|
||||||
|
};
|
||||||
|
|
||||||
const assets = await this.repository.find({
|
const assets = await this.repository.find({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -504,17 +519,10 @@ export class AssetRepository implements IAssetRepository {
|
||||||
longitude: true,
|
longitude: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: {
|
where: [
|
||||||
ownerId: In([...ownerIds]),
|
{ ...where, ownerId: In([...ownerIds]) },
|
||||||
isVisible: true,
|
{ ...where, albums: { id: In([...albumIds]) } },
|
||||||
isArchived,
|
],
|
||||||
exifInfo: {
|
|
||||||
latitude: Not(IsNull()),
|
|
||||||
longitude: Not(IsNull()),
|
|
||||||
},
|
|
||||||
isFavorite,
|
|
||||||
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
|
|
||||||
},
|
|
||||||
relations: {
|
relations: {
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
@ -18,6 +19,7 @@ import { faceStub } from 'test/fixtures/face.stub';
|
||||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
|
@ -160,6 +162,7 @@ describe(AssetService.name, () => {
|
||||||
let configMock: Mocked<ISystemConfigRepository>;
|
let configMock: Mocked<ISystemConfigRepository>;
|
||||||
let partnerMock: Mocked<IPartnerRepository>;
|
let partnerMock: Mocked<IPartnerRepository>;
|
||||||
let assetStackMock: Mocked<IAssetStackRepository>;
|
let assetStackMock: Mocked<IAssetStackRepository>;
|
||||||
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -182,6 +185,7 @@ describe(AssetService.name, () => {
|
||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
partnerMock = newPartnerRepositoryMock();
|
partnerMock = newPartnerRepositoryMock();
|
||||||
assetStackMock = newAssetStackRepositoryMock();
|
assetStackMock = newAssetStackRepositoryMock();
|
||||||
|
albumMock = newAlbumRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new AssetService(
|
sut = new AssetService(
|
||||||
|
@ -194,6 +198,7 @@ describe(AssetService.name, () => {
|
||||||
eventMock,
|
eventMock,
|
||||||
partnerMock,
|
partnerMock,
|
||||||
assetStackMock,
|
assetStackMock,
|
||||||
|
albumMock,
|
||||||
loggerMock,
|
loggerMock,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
import { LibraryType } from 'src/entities/library.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
@ -78,6 +79,7 @@ export class AssetService {
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
||||||
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(AssetService.name);
|
this.logger.setContext(AssetService.name);
|
||||||
|
@ -167,6 +169,7 @@ export class AssetService {
|
||||||
|
|
||||||
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||||
const userIds: string[] = [auth.user.id];
|
const userIds: string[] = [auth.user.id];
|
||||||
|
// TODO convert to SQL join
|
||||||
if (options.withPartners) {
|
if (options.withPartners) {
|
||||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||||
const partnersIds = partners
|
const partnersIds = partners
|
||||||
|
@ -174,7 +177,18 @@ export class AssetService {
|
||||||
.map((partner) => partner.sharedById);
|
.map((partner) => partner.sharedById);
|
||||||
userIds.push(...partnersIds);
|
userIds.push(...partnersIds);
|
||||||
}
|
}
|
||||||
return this.assetRepository.getMapMarkers(userIds, options);
|
|
||||||
|
// TODO convert to SQL join
|
||||||
|
const albumIds: string[] = [];
|
||||||
|
if (options.withSharedAlbums) {
|
||||||
|
const [ownedAlbums, sharedAlbums] = await Promise.all([
|
||||||
|
this.albumRepository.getOwned(auth.user.id),
|
||||||
|
this.albumRepository.getShared(auth.user.id),
|
||||||
|
]);
|
||||||
|
albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.assetRepository.getMapMarkers(userIds, albumIds, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
|
|
|
@ -30,7 +30,12 @@
|
||||||
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
|
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
|
||||||
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
|
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
|
||||||
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
|
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
|
||||||
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
|
<SettingSwitch
|
||||||
|
id="include-shared-partner-assets"
|
||||||
|
title="Include shared partner assets"
|
||||||
|
bind:checked={settings.withPartners}
|
||||||
|
/>
|
||||||
|
<SettingSwitch id="include-shared-albums" title="Include shared albums" bind:checked={settings.withSharedAlbums} />
|
||||||
{#if customDateRange}
|
{#if customDateRange}
|
||||||
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
||||||
<div class="flex items-center justify-between gap-8">
|
<div class="flex items-center justify-between gap-8">
|
||||||
|
|
|
@ -47,6 +47,7 @@ export interface MapSettings {
|
||||||
includeArchived: boolean;
|
includeArchived: boolean;
|
||||||
onlyFavorites: boolean;
|
onlyFavorites: boolean;
|
||||||
withPartners: boolean;
|
withPartners: boolean;
|
||||||
|
withSharedAlbums: boolean;
|
||||||
relativeDate: string;
|
relativeDate: string;
|
||||||
dateAfter: string;
|
dateAfter: string;
|
||||||
dateBefore: string;
|
dateBefore: string;
|
||||||
|
@ -57,6 +58,7 @@ export const mapSettings = persisted<MapSettings>('map-settings', {
|
||||||
includeArchived: false,
|
includeArchived: false,
|
||||||
onlyFavorites: false,
|
onlyFavorites: false,
|
||||||
withPartners: false,
|
withPartners: false,
|
||||||
|
withSharedAlbums: false,
|
||||||
relativeDate: '',
|
relativeDate: '',
|
||||||
dateAfter: '',
|
dateAfter: '',
|
||||||
dateBefore: '',
|
dateBefore: '',
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
}
|
}
|
||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
|
|
||||||
const { includeArchived, onlyFavorites, withPartners } = $mapSettings;
|
const { includeArchived, onlyFavorites, withPartners, withSharedAlbums } = $mapSettings;
|
||||||
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
|
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
|
||||||
|
|
||||||
return await getMapMarkers(
|
return await getMapMarkers(
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
fileCreatedAfter: fileCreatedAfter || undefined,
|
fileCreatedAfter: fileCreatedAfter || undefined,
|
||||||
fileCreatedBefore,
|
fileCreatedBefore,
|
||||||
withPartners: withPartners || undefined,
|
withPartners: withPartners || undefined,
|
||||||
|
withSharedAlbums: withSharedAlbums || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
|
|
Loading…
Reference in a new issue