1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-11-29 22:48:28 +02:00

Merge pull request #316 from bpatrik/feature/saved-search

Creating saved search as Albums #45
This commit is contained in:
Patrik J. Braun
2021-05-28 23:42:43 +02:00
committed by GitHub
44 changed files with 1154 additions and 120 deletions

View File

@@ -0,0 +1,63 @@
import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {ObjectManagers} from '../model/ObjectManagers';
import {Utils} from '../../common/Utils';
import {Config} from '../../common/config/private/Config';
export class AlbumMWs {
public static async listAlbums(req: Request, res: Response, next: NextFunction): Promise<void> {
if (Config.Client.Album.enabled === false) {
return next();
}
try {
req.resultPipe = await ObjectManagers.getInstance()
.AlbumManager.getAlbums();
return next();
} catch (err) {
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during listing albums', err));
}
}
public static async deleteAlbum(req: Request, res: Response, next: NextFunction): Promise<void> {
if (Config.Client.Album.enabled === false) {
return next();
}
if (!req.params.id || !Utils.isUInt32(parseInt(req.params.id, 10))) {
return next();
}
try {
await ObjectManagers.getInstance().AlbumManager.deleteAlbum(parseInt(req.params.id, 10));
req.resultPipe = 'ok';
return next();
} catch (err) {
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during deleting albums', err));
}
}
public static async createSavedSearch(req: Request, res: Response, next: NextFunction): Promise<void> {
if (Config.Client.Album.enabled === false) {
return next();
}
if ((typeof req.body === 'undefined') || (typeof req.body.name !== 'string') || (typeof req.body.searchQuery !== 'object')) {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing'));
}
try {
await ObjectManagers.getInstance().AlbumManager.addSavedSearch(req.body.name, req.body.searchQuery);
req.resultPipe = 'ok';
return next();
} catch (err) {
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during creating saved search albums', err));
}
}
}

View File

@@ -117,6 +117,7 @@ export class SharingMWs {
try {
req.resultPipe = await ObjectManagers.getInstance().SharingManager.deleteSharing(sharingKey);
req.resultPipe = 'ok';
return next();
} catch (err) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during deleting sharing', err));

View File

@@ -9,6 +9,8 @@ import {IPersonManager} from './database/interfaces/IPersonManager';
import {IVersionManager} from './database/interfaces/IVersionManager';
import {IJobManager} from './database/interfaces/IJobManager';
import {LocationManager} from './database/LocationManager';
import {IAlbumManager} from './database/interfaces/IAlbumManager';
import {JobManager} from './jobs/JobManager';
const LOG_TAG = '[ObjectManagers]';
@@ -25,6 +27,7 @@ export class ObjectManagers {
private versionManager: IVersionManager;
private jobManager: IJobManager;
private locationManager: LocationManager;
private albumManager: IAlbumManager;
get VersionManager(): IVersionManager {
@@ -43,6 +46,14 @@ export class ObjectManagers {
this.locationManager = value;
}
get AlbumManager(): IAlbumManager {
return this.albumManager;
}
set AlbumManager(value: IAlbumManager) {
this.albumManager = value;
}
get PersonManager(): IPersonManager {
return this.personManager;
}
@@ -121,51 +132,30 @@ export class ObjectManagers {
}
public static async InitCommonManagers(): Promise<void> {
const JobManager = require('./jobs/JobManager').JobManager;
ObjectManagers.getInstance().JobManager = new JobManager();
}
public static async InitMemoryManagers(): Promise<void> {
await ObjectManagers.reset();
const GalleryManager = require('./database/memory/GalleryManager').GalleryManager;
const UserManager = require('./database/memory/UserManager').UserManager;
const SearchManager = require('./database/memory/SearchManager').SearchManager;
const SharingManager = require('./database/memory/SharingManager').SharingManager;
const IndexingManager = require('./database/memory/IndexingManager').IndexingManager;
const PersonManager = require('./database/memory/PersonManager').PersonManager;
const VersionManager = require('./database/memory/VersionManager').VersionManager;
ObjectManagers.getInstance().GalleryManager = new GalleryManager();
ObjectManagers.getInstance().UserManager = new UserManager();
ObjectManagers.getInstance().SearchManager = new SearchManager();
ObjectManagers.getInstance().SharingManager = new SharingManager();
ObjectManagers.getInstance().IndexingManager = new IndexingManager();
ObjectManagers.getInstance().PersonManager = new PersonManager();
ObjectManagers.getInstance().VersionManager = new VersionManager();
ObjectManagers.getInstance().LocationManager = new LocationManager();
this.InitCommonManagers();
this.initManagers('memory');
Logger.debug(LOG_TAG, 'Memory DB inited');
}
public static async InitSQLManagers(): Promise<void> {
await ObjectManagers.reset();
await SQLConnection.init();
const GalleryManager = require('./database/sql/GalleryManager').GalleryManager;
const UserManager = require('./database/sql/UserManager').UserManager;
const SearchManager = require('./database/sql/SearchManager').SearchManager;
const SharingManager = require('./database/sql/SharingManager').SharingManager;
const IndexingManager = require('./database/sql/IndexingManager').IndexingManager;
const PersonManager = require('./database/sql/PersonManager').PersonManager;
const VersionManager = require('./database/sql/VersionManager').VersionManager;
ObjectManagers.getInstance().GalleryManager = new GalleryManager();
ObjectManagers.getInstance().UserManager = new UserManager();
ObjectManagers.getInstance().SearchManager = new SearchManager();
ObjectManagers.getInstance().SharingManager = new SharingManager();
ObjectManagers.getInstance().IndexingManager = new IndexingManager();
ObjectManagers.getInstance().PersonManager = new PersonManager();
ObjectManagers.getInstance().VersionManager = new VersionManager();
ObjectManagers.getInstance().LocationManager = new LocationManager();
this.InitCommonManagers();
this.initManagers('sql');
Logger.debug(LOG_TAG, 'SQL DB inited');
}
private static initManagers(type: 'memory' | 'sql'): void {
ObjectManagers.getInstance().AlbumManager = new (require(`./database/${type}/AlbumManager`).AlbumManager)();
ObjectManagers.getInstance().GalleryManager = new (require(`./database/${type}/GalleryManager`).GalleryManager)();
ObjectManagers.getInstance().IndexingManager = new (require(`./database/${type}/IndexingManager`).IndexingManager)();
ObjectManagers.getInstance().PersonManager = new (require(`./database/${type}/PersonManager`).PersonManager)();
ObjectManagers.getInstance().SearchManager = new (require(`./database/${type}/SearchManager`).SearchManager)();
ObjectManagers.getInstance().SharingManager = new (require(`./database/${type}/SharingManager`).SharingManager)();
ObjectManagers.getInstance().UserManager = new (require(`./database/${type}/UserManager`).UserManager)();
ObjectManagers.getInstance().VersionManager = new (require(`./database/${type}/VersionManager`).VersionManager)();
ObjectManagers.getInstance().JobManager = new JobManager();
ObjectManagers.getInstance().LocationManager = new LocationManager();
}
}

View File

@@ -0,0 +1,19 @@
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
export interface IAlbumManager {
/**
* Creates a saved search type of album
*/
addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void>;
/**
* Deletes an album
*/
deleteAlbum(id: number): Promise<void>;
/**
* Returns with all albums
*/
getAlbums(): Promise<AlbumBaseDTO[]>;
}

View File

@@ -0,0 +1,19 @@
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {IAlbumManager} from '../interfaces/IAlbumManager';
export class AlbumManager implements IAlbumManager {
public async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
throw new Error('not supported by memory DB');
}
public async deleteAlbum(id: number): Promise<void> {
throw new Error('not supported by memory DB');
}
public async getAlbums(): Promise<AlbumBaseDTO[]> {
throw new Error('not supported by memory DB');
}
}

View File

@@ -0,0 +1,40 @@
import {SQLConnection} from './SQLConnection';
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
import {ObjectManagers} from '../../ObjectManagers';
import {ISQLSearchManager} from './ISearchManager';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
import { IAlbumManager } from '../interfaces/IAlbumManager';
export class AlbumManager implements IAlbumManager{
private static async fillPreviewToAlbum(album: AlbumBaseDTO): Promise<void> {
if (!(album as SavedSearchDTO).searchQuery) {
throw new Error('no search query present');
}
album.preview = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
.getPreview((album as SavedSearchDTO).searchQuery);
}
public async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
const connection = await SQLConnection.getConnection();
await connection.getRepository(SavedSearchEntity).insert({name, searchQuery});
}
public async deleteAlbum(id: number): Promise<void> {
const connection = await SQLConnection.getConnection();
await connection.getRepository(AlbumBaseEntity).delete({id});
}
public async getAlbums(): Promise<AlbumBaseDTO[]> {
const connection = await SQLConnection.getConnection();
const albums = await connection.getRepository(AlbumBaseEntity).find();
for (const a of albums) {
await AlbumManager.fillPreviewToAlbum(a);
}
return albums;
}
}

View File

@@ -0,0 +1,17 @@
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {ISearchManager} from '../interfaces/ISearchManager';
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
export interface ISQLSearchManager extends ISearchManager {
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]>;
search(query: SearchQueryDTO): Promise<SearchResultDTO>;
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO>;
// "Protected" functions. only called from other Managers, not from middlewares
getPreview(query: SearchQueryDTO): Promise<MediaDTO>;
}

View File

@@ -19,6 +19,8 @@ import {PersonEntry} from './enitites/PersonEntry';
import {Utils} from '../../../../common/Utils';
import * as path from 'path';
import {DatabaseType, ServerDataBaseConfig, SQLLogLevel} from '../../../../common/config/private/PrivateConfig';
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
export class SQLConnection {
@@ -43,6 +45,8 @@ export class SQLConnection {
VideoEntity,
DirectoryEntity,
SharingEntity,
AlbumBaseEntity,
SavedSearchEntity,
VersionEntity
];
options.synchronize = false;
@@ -73,6 +77,8 @@ export class SQLConnection {
VideoEntity,
DirectoryEntity,
SharingEntity,
AlbumBaseEntity,
SavedSearchEntity,
VersionEntity
];
options.synchronize = false;

View File

@@ -1,5 +1,4 @@
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {ISearchManager} from '../interfaces/ISearchManager';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
import {SQLConnection} from './SQLConnection';
import {PhotoEntity} from './enitites/PhotoEntity';
@@ -32,8 +31,10 @@ import {Utils} from '../../../../common/Utils';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
import {ISQLGalleryManager} from './IGalleryManager';
import {ISQLSearchManager} from './ISearchManager';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
export class SearchManager implements ISearchManager {
export class SearchManager implements ISQLSearchManager {
private static autoCompleteItemsUnique(array: Array<AutoCompleteItem>): Array<AutoCompleteItem> {
const a = array.concat();
@@ -223,6 +224,21 @@ export class SearchManager implements ISearchManager {
}
public async getPreview(queryIN: SearchQueryDTO): Promise<MediaDTO> {
let query = this.flattenSameOfQueries(queryIN);
query = await this.getGPSData(query);
const connection = await SQLConnection.getConnection();
return await connection
.getRepository(MediaEntity)
.createQueryBuilder('media')
.innerJoinAndSelect('media.directory', 'directory')
.where(this.buildWhereQuery(query))
.orderBy('media.metadata.creationDate', 'DESC')
.limit(1)
.getOne();
}
/**
* Returns only those part of a query tree that only contains directory related search queries
*/
@@ -632,4 +648,5 @@ export class SearchManager implements ISearchManager {
return res;
}
}

View File

@@ -82,7 +82,7 @@ export class MediaMetadataEntity implements MediaMetadata {
// TODO: fix inheritance once its working in typeorm
@Entity()
@Unique(['name', 'directory'])
@TableInheritance({column: {type: 'varchar', name: 'type', length: 32}})
@TableInheritance({column: {type: 'varchar', name: 'type', length: 16}})
export abstract class MediaEntity implements MediaDTO {
@Index()

View File

@@ -0,0 +1,21 @@
import {Column, Entity, Index, PrimaryGeneratedColumn, TableInheritance} from 'typeorm';
import {MediaEntity} from '../MediaEntity';
import {columnCharsetCS} from '../EntityUtils';
import {AlbumBaseDTO} from '../../../../../../common/entities/album/AlbumBaseDTO';
@Entity()
@TableInheritance({column: {type: 'varchar', name: 'type', length: 24}})
export class AlbumBaseEntity implements AlbumBaseDTO {
@Index()
@PrimaryGeneratedColumn({unsigned: true})
id: number;
@Index()
@Column(columnCharsetCS)
name: string;
// not saving to database, it is only assigned when querying the DB
public preview: MediaEntity;
}

View File

@@ -0,0 +1,23 @@
import {ChildEntity, Column} from 'typeorm';
import {AlbumBaseEntity} from './AlbumBaseEntity';
import {SavedSearchDTO} from '../../../../../../common/entities/album/SavedSearchDTO';
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
@ChildEntity()
export class SavedSearchEntity extends AlbumBaseEntity implements SavedSearchDTO {
@Column({
type: 'text',
nullable: false,
transformer: {
// used to deserialize your data from db field value
from: (val: string) => {
return JSON.parse(val);
},
// used to serialize your data to db field
to: (val: object) => {
return JSON.stringify(val);
}
}
})
searchQuery: SearchQueryDTO;
}

View File

@@ -0,0 +1,56 @@
import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs';
import {Express} from 'express';
import {RenderingMWs} from '../middlewares/RenderingMWs';
import {UserRoles} from '../../common/entities/UserDTO';
import {VersionMWs} from '../middlewares/VersionMWs';
import {AlbumMWs} from '../middlewares/AlbumMWs';
export class AlbumRouter {
public static route(app: Express): void {
this.addListAlbums(app);
this.addAddSavedSearch(app);
this.addDeleteAlbum(app);
}
private static addListAlbums(app: Express): void {
app.get(['/api/albums'],
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User),
VersionMWs.injectGalleryVersion,
// specific part
AlbumMWs.listAlbums,
RenderingMWs.renderResult
);
}
private static addDeleteAlbum(app: Express): void {
app.delete(['/api/albums/:id'],
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
VersionMWs.injectGalleryVersion,
// specific part
AlbumMWs.deleteAlbum,
RenderingMWs.renderResult
);
}
private static addAddSavedSearch(app: Express): void {
app.put(['/api/albums/saved-searches'],
// common part
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
VersionMWs.injectGalleryVersion,
// specific part
AlbumMWs.createSavedSearch,
RenderingMWs.renderResult
);
}
}

View File

@@ -108,7 +108,7 @@ export class PublicRouter {
}
);
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/search*'],
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/albums', '/search*'],
AuthenticationMWs.tryAuthenticate,
setLocale,
renderIndex

View File

@@ -8,6 +8,7 @@ import {AdminRouter} from './admin/AdminRouter';
import {SettingsRouter} from './admin/SettingsRouter';
import {NotificationRouter} from './NotificationRouter';
import {ErrorRouter} from './ErrorRouter';
import {AlbumRouter} from './AlbumRouter';
export class Router {
@@ -15,13 +16,14 @@ export class Router {
PublicRouter.route(app);
UserRouter.route(app);
GalleryRouter.route(app);
PersonRouter.route(app);
SharingRouter.route(app);
AdminRouter.route(app);
SettingsRouter.route(app);
AlbumRouter.route(app);
GalleryRouter.route(app);
NotificationRouter.route(app);
PersonRouter.route(app);
SettingsRouter.route(app);
SharingRouter.route(app);
UserRouter.route(app);
ErrorRouter.route(app);
}

View File

@@ -57,7 +57,7 @@ export class SharingRouter {
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
SharingMWs.deleteSharing,
RenderingMWs.renderOK
RenderingMWs.renderResult
);
}

View File

@@ -1 +1 @@
export const DataStructureVersion = 21;
export const DataStructureVersion = 22;

View File

@@ -35,6 +35,13 @@ export class ClientSearchConfig {
maxDirectoryResult: number = 200;
}
@SubConfigClass()
export class ClientAlbumConfig {
@ConfigProperty()
enabled: boolean = false;
}
@SubConfigClass()
export class ClientSharingConfig {
@ConfigProperty()
@@ -172,6 +179,8 @@ export class ClientConfig {
@ConfigProperty()
Sharing: ClientSharingConfig = new ClientSharingConfig();
@ConfigProperty()
Album: ClientAlbumConfig = new ClientAlbumConfig();
@ConfigProperty()
Map: ClientMapConfig = new ClientMapConfig();
@ConfigProperty()
RandomPhoto: ClientRandomPhotoConfig = new ClientRandomPhotoConfig();

View File

@@ -24,6 +24,8 @@ export enum ErrorCodes {
TASK_ERROR = 14,
JOB_ERROR = 15,
LocationLookUp_ERROR = 16,
ALBUM_ERROR = 17,
}
export class ErrorDTO {

View File

@@ -158,7 +158,7 @@ export interface TextSearch extends NegatableSearchQuery {
SearchQueryTypes.caption |
SearchQueryTypes.file_name |
SearchQueryTypes.directory;
matchType: TextSearchQueryMatchTypes;
matchType?: TextSearchQueryMatchTypes;
text: string;
}

View File

@@ -0,0 +1,7 @@
import {PreviewPhotoDTO} from '../PhotoDTO';
export interface AlbumBaseDTO {
id: number;
name: string;
preview: PreviewPhotoDTO;
}

View File

@@ -0,0 +1,11 @@
import {AlbumBaseDTO} from './AlbumBaseDTO';
import {PreviewPhotoDTO} from '../PhotoDTO';
import {SearchQueryDTO} from '../SearchQueryDTO';
export interface SavedSearchDTO extends AlbumBaseDTO {
id: number;
name: string;
preview: PreviewPhotoDTO;
searchQuery: SearchQueryDTO;
}

View File

@@ -100,6 +100,10 @@ import {AppRoutingModule} from './app.routing';
import {CookieService} from 'ngx-cookie-service';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
import {icon, Marker} from 'leaflet';
import {AlbumsComponent} from './ui/albums/albums.component';
import {AlbumComponent} from './ui/albums/album/album.component';
import {AlbumsService} from './ui/albums/albums.service';
import {GallerySearchQueryBuilderComponent} from './ui/gallery/search/query-builder/query-bulder.gallery.component';
@Injectable()
@@ -178,6 +182,9 @@ Marker.prototype.options.icon = iconDefault;
LanguageComponent,
TimeStampDatePickerComponent,
TimeStampTimePickerComponent,
// Albums
AlbumsComponent,
AlbumComponent,
// Gallery
GalleryLightboxMediaComponent,
GalleryPhotoLoadingComponent,
@@ -190,6 +197,7 @@ Marker.prototype.options.icon = iconDefault;
GallerySearchComponent,
GallerySearchQueryEntryComponent,
GallerySearchFieldComponent,
GallerySearchQueryBuilderComponent,
GalleryShareComponent,
GalleryNavigatorComponent,
GalleryPhotoComponent,
@@ -241,6 +249,7 @@ Marker.prototype.options.icon = iconDefault;
NetworkService,
ShareService,
UserService,
AlbumsService,
GalleryCacheService,
GalleryService,
MapService,

View File

@@ -8,6 +8,7 @@ import {QueryParams} from '../../common/QueryParams';
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
import {FacesComponent} from './ui/faces/faces.component';
import {AuthGuard} from './model/network/helper/auth.guard';
import {AlbumsComponent} from './ui/albums/albums.component';
export function galleryMatcherFunction(
segments: UrlSegment[]): UrlMatchResult | null {
@@ -59,6 +60,11 @@ const routes: Routes = [
component: DuplicateComponent,
canActivate: [AuthGuard]
},
{
path: 'albums',
component: AlbumsComponent,
canActivate: [AuthGuard]
},
{
path: 'faces',
component: FacesComponent,

View File

@@ -0,0 +1,60 @@
.delete {
margin: 2px;
cursor: default;
}
.delete {
cursor: pointer;
transition: all .05s ease-in-out;
transform: scale(1.0, 1.0);
}
.delete:hover {
transform: scale(1.4, 1.4);
}
a {
position: relative;
}
.photo-container {
border: 2px solid #333;
width: 180px;
height: 180px;
background-color: #bbbbbb;
}
.no-image {
position: absolute;
color: #7f7f7f;
font-size: 80px;
top: calc(50% - 40px);
left: calc(50% - 40px);
}
.photo {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
.info {
background-color: rgba(0, 0, 0, 0.6);
color: white;
font-size: medium;
position: absolute;
bottom: 0;
left: 0;
padding: 5px;
width: 100%;
}
a:hover .info {
background-color: rgba(0, 0, 0, 0.8);
}
a:hover .photo-container {
border-color: #000;
}

View File

@@ -0,0 +1,28 @@
<a [routerLink]="RouterLink"
style="display: inline-block;">
<div class="photo-container"
[style.width.px]="size"
[style.height.px]="size">
<div class="photo"
*ngIf="thumbnail && thumbnail.Available"
[style.background-image]="getSanitizedThUrl()"></div>
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"
aria-hidden="true">
</span>
</div>
<!--Info box -->
<div class="info">
{{album.name}}
<span *ngIf="CanUpdate"
(click)="deleteAlbum($event)"
class="delete oi oi-trash float-right"></span>
</div>
</a>

View File

@@ -0,0 +1,77 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {RouterLink} from '@angular/router';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {Thumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {AlbumsService} from '../albums.service';
import {AlbumBaseDTO} from '../../../../../common/entities/album/AlbumBaseDTO';
import {Media} from '../../gallery/Media';
import {SavedSearchDTO} from '../../../../../common/entities/album/SavedSearchDTO';
import {UserRoles} from '../../../../../common/entities/UserDTO';
@Component({
selector: 'app-album',
templateUrl: './album.component.html',
styleUrls: ['./album.component.css'],
providers: [RouterLink],
})
export class AlbumComponent implements OnInit, OnDestroy {
@Input() album: AlbumBaseDTO;
@Input() size: number;
public thumbnail: Thumbnail = null;
constructor(private thumbnailService: ThumbnailManagerService,
private sanitizer: DomSanitizer,
private albumService: AlbumsService,
public authenticationService: AuthenticationService) {
}
get IsSavedSearch(): boolean {
return this.album && !!this.AsSavedSearch.searchQuery;
}
get AsSavedSearch(): SavedSearchDTO {
return this.album as SavedSearchDTO;
}
get CanUpdate(): boolean {
return this.authenticationService.user.getValue().role >= UserRoles.Admin;
}
get RouterLink(): any[] {
if (this.IsSavedSearch) {
return ['/search', JSON.stringify(this.AsSavedSearch.searchQuery)];
}
// TODO: add "normal" albums here once they are ready, see: https://github.com/bpatrik/pigallery2/issues/301
return null;
}
ngOnInit(): void {
if (this.album.preview) {
this.thumbnail = this.thumbnailService.getThumbnail(new Media(this.album.preview, this.size, this.size));
}
}
getSanitizedThUrl(): SafeStyle {
return this.sanitizer.bypassSecurityTrustStyle('url(' + this.thumbnail.Src
.replace(/\(/g, '%28')
.replace(/'/g, '%27')
.replace(/\)/g, '%29') + ')');
}
ngOnDestroy(): void {
if (this.thumbnail != null) {
this.thumbnail.destroy();
}
}
async deleteAlbum($event: MouseEvent): Promise<void> {
$event.preventDefault();
$event.stopPropagation();
await this.albumService.deleteAlbum(this.album).catch(console.error);
}
}

View File

@@ -0,0 +1,29 @@
app-album {
margin: 2px;
display: inline-block;
}
.no-item-msg {
height: 100vh;
text-align: center;
}
.no-face-msg h2 {
color: #6c757d;
}
.add-saved-search {
vertical-align: baseline;
position: absolute;
margin: 2px;
}
.add-saved-search .text {
position: relative;
top: calc(50% - 40px);
text-align: center;
}
.add-saved-search .text .oi {
font-size: 80px;
}

View File

@@ -0,0 +1,74 @@
<app-frame>
<div body #container class="container-fluid">
<app-album *ngFor="let album of albumsService.albums | async"
[album]="album"
[size]="size"></app-album>
<div class="add-saved-search btn btn-secondary"
[style.width.px]="size"
[style.height.px]="size"
(click)="openModal(modal)">
<div class="text">
<span class="oi oi-plus" aria-hidden="true"> </span><br/>
<span i18n>Add saved search</span>
</div>
</div>
<div class="d-flex no-item-msg"
*ngIf="(albumsService.albums | async) && (albumsService.albums | async).length == 0">
<div class="flex-fill">
<h2>:(
<ng-container i18n>No albums to show.</ng-container>
</h2>
</div>
</div>
</div>
</app-frame>
<ng-template #modal>
<!-- sharing Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>Add Saved Search</h5>
<button type="button" class="close" (click)="hideModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #savedSearchPanelForm="ngForm" class="form-horizontal">
<div class="row">
</div>
<div class="form-group">
<label for="saveSearchName">Album name</label>
<input
id="saveSearchName"
name="saveSearchName"
placeholder="Search text"
class="form-control input-md"
[(ngModel)]="savedSearch.name"
type="text"/>
</div>
<div class="form-group">
<label for="album-search-query-builder">Search query</label>
<app-gallery-search-query-builder
id="album-search-query-builder"
name="album-search-query-builder"
[(ngModel)]="savedSearch.searchQuery">
</app-gallery-search-query-builder>
</div>
<div class="input-group-btn float-right row" style="display: block">
<button class="btn btn-primary" type="button"
[disabled]="savedSearch.searchQuery.text == ''"
(click)="saveSearch()">
<span class="oi oi-folder"></span> Save
</button>
</div>
</form>
</div>
</ng-template>

View File

@@ -0,0 +1,56 @@
import {Component, ElementRef, OnInit, TemplateRef, ViewChild} from '@angular/core';
import {AlbumsService} from './albums.service';
import {BsModalService} from 'ngx-bootstrap/modal';
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
import {SearchQueryTypes, TextSearch} from '../../../../common/entities/SearchQueryDTO';
@Component({
selector: 'app-albums',
templateUrl: './albums.component.html',
styleUrls: ['./albums.component.css']
})
export class AlbumsComponent implements OnInit {
@ViewChild('container', {static: true}) container: ElementRef;
public size: number;
public savedSearch = {
name: '',
searchQuery: {type: SearchQueryTypes.any_text, text: ''} as TextSearch
};
private modalRef: BsModalRef;
constructor(public albumsService: AlbumsService,
private modalService: BsModalService) {
this.albumsService.getAlbums().catch(console.error);
}
ngOnInit(): void {
this.updateSize();
}
public async openModal(template: TemplateRef<any>): Promise<void> {
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
document.body.style.paddingRight = '0px';
}
public hideModal(): void {
this.modalRef.hide();
this.modalRef = null;
}
async saveSearch(): Promise<void> {
await this.albumsService.addSavedSearch(this.savedSearch.name, this.savedSearch.searchQuery);
this.hideModal();
}
private updateSize(): void {
const size = 220 + 5;
// body - container margin
const containerWidth = this.container.nativeElement.clientWidth - 30;
this.size = (containerWidth / Math.round((containerWidth / size))) - 5;
}
}

View File

@@ -0,0 +1,31 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service';
import {BehaviorSubject} from 'rxjs';
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
@Injectable()
export class AlbumsService {
public albums: BehaviorSubject<AlbumBaseDTO[]>;
constructor(private networkService: NetworkService) {
this.albums = new BehaviorSubject<AlbumBaseDTO[]>(null);
}
public async getAlbums(): Promise<void> {
this.albums.next((await this.networkService.getJson<AlbumBaseDTO[]>('/albums'))
.sort((a, b): number => a.name.localeCompare(b.name)));
}
async deleteAlbum(album: AlbumBaseDTO): Promise<void> {
await this.networkService.deleteJson('/albums/' + album.id);
await this.getAlbums();
}
async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
await this.networkService.putJson('/albums/saved-searches', {name, searchQuery});
await this.getAlbums();
}
}

View File

@@ -72,8 +72,3 @@ a:hover .photo-container {
border-color: #000;
}
.person-name {
display: inline-block;
width: 180px;
white-space: normal;
}

View File

@@ -17,6 +17,9 @@
[routerLink]="['/gallery']"
[queryParams]="queryService.getParams()" [class.active]="isLinkActive('/gallery')" i18n>Gallery</a>
</li>
<li class="nav-item" *ngIf="isAlbumsAvailable()">
<a class="nav-link" [routerLink]="['/albums']" [class.active]="isLinkActive('/albums')" i18n>Albums</a>
</li>
<li class="nav-item" *ngIf="isFacesAvailable()">
<a class="nav-link" [routerLink]="['/faces']" [class.active]="isLinkActive('/faces')" i18n>Faces</a>
</li>

View File

@@ -45,5 +45,8 @@ export class FrameComponent {
this.authService.logout();
}
isAlbumsAvailable(): boolean {
return Config.Client.Album.enabled;
}
}

View File

@@ -59,15 +59,5 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
}
}
/*
calcSize() {
if (this.size == null || PageHelper.isScrollYVisible()) {
const size = 220 + 5;
const containerWidth = this.container.nativeElement.parentElement.parentElement.clientWidth;
this.size = containerWidth / Math.round((containerWidth / size));
}
return Math.floor(this.size - 5);
}
*/
}

View File

@@ -32,19 +32,12 @@
</div>
<hr/>
<form #searchPanelForm="ngForm" class="form-horizontal">
<app-gallery-search-field [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
name="form-search-field">
</app-gallery-search-field>
<app-gallery-search-query-entry
<app-gallery-search-query-builder
id="album-search-query-builder"
name="album-search-query-builder"
[(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
name="search-root"
(delete)="resetQuery()">
</app-gallery-search-query-entry>
(change)="onQueryChange()" >
</app-gallery-search-query-builder>
</form>

View File

@@ -20,8 +20,7 @@ import {SearchQueryParserService} from '../search/search-query-parser.service';
})
export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
public searchQueryDTO: SearchQueryDTO;
public rawSearchText: string;
public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
enabled = true;
url = '';
@@ -37,7 +36,6 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
private searchQueryParserService: SearchQueryParserService,
private route: ActivatedRoute,
private modalService: BsModalService) {
this.resetQuery();
this.subscription = this.route.params.subscribe((params: Params) => {
if (!params[QueryParams.gallery.search.query]) {
@@ -55,17 +53,8 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
return JSON.stringify(this.searchQueryDTO);
}
validateRawSearchText(): void {
try {
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
} catch (e) {
console.error(e);
}
}
onQueryChange(): void {
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
}
@@ -114,9 +103,4 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
}
resetQuery(): void {
this.searchQueryDTO = {text: '', type: SearchQueryTypes.any_text} as TextSearch;
}
}

View File

@@ -0,0 +1,69 @@
.autocomplete-list {
position: absolute;
left: 0;
top: 34px;
background-color: white;
width: 100%;
border: 1px solid #ccc;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 5px 0;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
z-index: 7;
}
.insert-button {
margin-right: -15px;
display: none;
margin-top: 2px;
}
.autocomplete-item-selected .insert-button {
display: block;
}
@media (hover: none) {
.insert-button {
display: block;
}
}
.autocomplete-item-selected .insert-button:hover {
color: black;
}
.autocomplete-item {
cursor: pointer;
padding-top: 2px;
padding-bottom: 2px;
font-size: 17px;
}
.autocomplete-item {
color: #333;
padding: 0 20px;
line-height: 1.42857143;
font-weight: 400;
display: block;
}
.autocomplete-item-selected {
background-color: #007bff;
color: #FFF;
}
.search-text {
z-index: 6;
width: 100%;
background: transparent;
}
.search-hint {
left: 0;
z-index: 1;
width: 100%;
position: absolute;
margin-left: 0 !important;
}

View File

@@ -0,0 +1,15 @@
<app-gallery-search-field [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
(search)="search.emit()"
name="form-search-field">
</app-gallery-search-field>
<hr>
<app-gallery-search-query-entry
[(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
(ngModelChange)="onChange()"
name="search-root"
(delete)="resetQuery()">
</app-gallery-search-query-entry>

View File

@@ -0,0 +1,82 @@
import {Component, EventEmitter, forwardRef, Output} from '@angular/core';
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../../common/entities/SearchQueryDTO';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
import {SearchQueryParserService} from '../search-query-parser.service';
@Component({
selector: 'app-gallery-search-query-builder',
templateUrl: './query-builder.gallery.component.html',
styleUrls: ['./query-builder.gallery.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GallerySearchQueryBuilderComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GallerySearchQueryBuilderComponent),
multi: true
}
]
})
export class GallerySearchQueryBuilderComponent implements ControlValueAccessor, Validator {
public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
@Output() search = new EventEmitter<void>();
public rawSearchText = '';
constructor(
private searchQueryParserService: SearchQueryParserService) {
}
validateRawSearchText(): void {
try {
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
this.onChange();
} catch (e) {
console.error(e);
}
}
resetQuery(): void {
this.searchQueryDTO = ({text: '', type: SearchQueryTypes.any_text} as TextSearch);
}
onQueryChange(): void {
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
this.onChange();
}
validate(control: FormControl): ValidationErrors {
return {required: true};
}
public onTouched(): void {
}
public writeValue(obj: any): void {
this.searchQueryDTO = obj;
}
registerOnChange(fn: (_: any) => void): void {
this.propagateChange = fn;
}
registerOnTouched(fn: () => void): void {
this.propagateTouch = fn;
}
public onChange(): void {
this.propagateChange(this.searchQueryDTO);
}
private propagateChange = (_: any): void => {
};
private propagateTouch = (_: any): void => {
};
}

View File

@@ -1,11 +1,11 @@
<form class="navbar-form" role="search" #SearchForm="ngForm">
<div class="input-group">
<app-gallery-search-field [(ngModel)]="rawSearchText"
<app-gallery-search-field [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
class="search-field"
(search)="Search()"
name="search-field">
class="search-field"
(search)="Search()"
name="search-field">
</app-gallery-search-field>
@@ -17,7 +17,7 @@
</div>
<div class="input-group-btn" style="display: block">
<button class="btn btn-light" type="button" (click)="openModal(searchModal)">
<button class="btn btn-light" type="button" (click)="openSearchModal(searchModal)">
<span class="oi oi-chevron-bottom"></span>
</button>
</div>
@@ -29,34 +29,65 @@
<!-- sharing Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>Search</h5>
<button type="button" class="close" (click)="hideModal()" data-dismiss="modal" aria-label="Close">
<button type="button" class="close" (click)="hideSearchModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #searchPanelForm="ngForm" class="form-horizontal">
<app-gallery-search-field [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
(search)="Search()"
name="form-search-field">
</app-gallery-search-field>
<app-gallery-search-query-entry
<app-gallery-search-query-builder
name="search-query-builder"
[(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
name="search-root"
(delete)="resetQuery()">
(search)="Search()">
</app-gallery-search-query-builder>
</app-gallery-search-query-entry>
<div class="input-group-btn float-right" style="display: block">
<div class="input-group-btn float-right row" style="display: block">
<button *ngIf="AlbumsEnabled"
class="btn btn-secondary mr-2" type="button"
[disabled]="rawSearchText == ''"
(click)="openSaveSearchModal(saveSearchModal)">
<span class="oi oi-folder"></span> Save
</button>
<button class="btn btn-primary" type="button"
[routerLink]="['/search', HTMLSearchQuery]"
(click)="hideModal()">
(click)="hideSearchModal()">
<span class="oi oi-magnifying-glass"></span> Search
</button>
</div>
</form>
</div>
</ng-template>
<ng-template #saveSearchModal>
<!-- sharing Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>Save search to album</h5>
<button type="button" class="close" (click)="hideSaveSearchModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #saveSearchPanelForm="ngForm" class="form-horizontal">
<div class="row">
<input
id="saveSearchName"
name="saveSearchName"
placeholder="Search text"
class="form-control input-md"
[(ngModel)]="saveSearchName"
type="text"/>
</div>
<div class="input-group-btn float-right row mt-2" style="display: block">
<button class="btn btn-primary" type="button"
(click)="saveSearch()">
<span class="oi oi-folder"></span> Save as album
</button>
</div>
</form>
</div>
</ng-template>

View File

@@ -9,6 +9,8 @@ import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch}
import {BsModalService} from 'ngx-bootstrap/modal';
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
import {SearchQueryParserService} from './search-query-parser.service';
import {AlbumsService} from '../../albums/albums.service';
import {Config} from '../../../../../common/config/public/Config';
@Component({
selector: 'app-gallery-search',
@@ -22,13 +24,17 @@ export class GallerySearchComponent implements OnDestroy {
public rawSearchText = '';
mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes;
modalRef: BsModalRef;
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
public saveSearchName: string;
AlbumsEnabled = Config.Client.Album.enabled;
private searchModalRef: BsModalRef;
private readonly subscription: Subscription = null;
private saveSearchModalRef: BsModalRef;
constructor(private autoCompleteService: AutoCompleteService,
private searchQueryParserService: SearchQueryParserService,
private galleryService: GalleryService,
private albumService: AlbumsService,
private navigationService: NavigationService,
private route: ActivatedRoute,
public router: Router,
@@ -60,25 +66,34 @@ export class GallerySearchComponent implements OnDestroy {
}
}
public async openModal(template: TemplateRef<any>): Promise<void> {
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
public async openSearchModal(template: TemplateRef<any>): Promise<void> {
this.searchModalRef = this.modalService.show(template, {class: 'modal-lg'});
document.body.style.paddingRight = '0px';
}
public hideModal(): void {
this.modalRef.hide();
this.modalRef = null;
public hideSearchModal(): void {
this.searchModalRef.hide();
this.searchModalRef = null;
}
resetQuery(): void {
this.searchQueryDTO = ({text: '', type: SearchQueryTypes.any_text} as TextSearch);
public async openSaveSearchModal(template: TemplateRef<any>): Promise<void> {
this.saveSearchModalRef = this.modalService.show(template, {class: 'modal-lg'});
document.body.style.paddingRight = '0px';
}
public hideSaveSearchModal(): void {
this.saveSearchModalRef.hide();
this.saveSearchModalRef = null;
}
onQueryChange(): void {
console.log('cahnge', this.searchQueryDTO);
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
// this.validateRawSearchText();
}
validateRawSearchText(): void {
try {
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
@@ -92,6 +107,10 @@ export class GallerySearchComponent implements OnDestroy {
}
async saveSearch(): Promise<void> {
await this.albumService.addSavedSearch(this.saveSearchName, this.searchQueryDTO);
this.hideSaveSearchModal();
}
}

View File

@@ -38,7 +38,7 @@ export class DBTestHelper {
static enable = {
memory: false,
sqlite: true,
sqlite: process.env.TEST_SQLITE !== 'false',
mysql: process.env.TEST_MYSQL !== 'false'
};
public static readonly savedDescribe = savedDescribe;
@@ -144,6 +144,7 @@ export class DBTestHelper {
}
private async resetMySQL(): Promise<void> {
await ObjectManagers.reset();
Config.Server.Database.type = DatabaseType.mysql;
Config.Server.Database.mysql.database = 'pigallery2_test';
const conn = await SQLConnection.getConnection();

View File

@@ -0,0 +1,176 @@
import {DBTestHelper} from '../../../DBTestHelper';
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
import {TestHelper} from './TestHelper';
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
import {PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO';
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager';
import {SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO';
import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection';
import {AlbumBaseEntity} from '../../../../../src/backend/model/database/sql/enitites/album/AlbumBaseEntity';
import {Utils} from '../../../../../src/common/Utils';
import {MediaDTO} from '../../../../../src/common/entities/MediaDTO';
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
const chai = require('chai');
chai.use(deepEqualInAnyOrder);
const {expect} = chai;
// to help WebStorm to handle the test cases
declare let describe: any;
declare const after: any;
declare const before: any;
const tmpDescribe = describe;
describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
describe('AlbumManager', (sqlHelper: DBTestHelper) => {
describe = tmpDescribe;
/**
* dir
* |- v
* |- p
* |- p2
* |-> subDir
* |- p3
* |-> subDir2
* |- p4
*/
let dir: DirectoryDTO;
let subDir: DirectoryDTO;
let subDir2: DirectoryDTO;
let v: VideoDTO;
let p: PhotoDTO;
let p2: PhotoDTO;
let p3: PhotoDTO;
let p4: PhotoDTO;
const setUpTestGallery = async (): Promise<void> => {
const directory: DirectoryDTO = TestHelper.getDirectoryEntry();
subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace');
subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi');
p = TestHelper.getRandomizedPhotoEntry(directory, 'Photo1');
p2 = TestHelper.getRandomizedPhotoEntry(directory, 'Photo2');
p3 = TestHelper.getRandomizedPhotoEntry(subDir, 'Photo3');
p4 = TestHelper.getRandomizedPhotoEntry(subDir2, 'Photo4');
v = TestHelper.getVideoEntry1(directory);
dir = await DBTestHelper.persistTestDir(directory);
subDir = dir.directories[0];
subDir2 = dir.directories[1];
p = (dir.media.filter(m => m.name === p.name)[0] as any);
p2 = (dir.media.filter(m => m.name === p2.name)[0] as any);
v = (dir.media.filter(m => m.name === v.name)[0] as any);
p3 = (dir.directories[0].media[0] as any);
p4 = (dir.directories[1].media[0] as any);
};
const setUpSqlDB = async () => {
await sqlHelper.initDB();
await setUpTestGallery();
await ObjectManagers.InitSQLManagers();
};
const toAlbumPreview = (m: MediaDTO): MediaDTO => {
const tmpM = m.directory.media;
const tmpD = m.directory.directories;
const tmpP = m.directory.preview;
const tmpMT = m.directory.metaFile;
delete m.directory.directories;
delete m.directory.media;
delete m.directory.preview;
delete m.directory.metaFile;
const ret = Utils.clone(m);
delete (ret.metadata as PhotoMetadata).faces;
m.directory.directories = tmpD;
m.directory.media = tmpM;
m.directory.preview = tmpP;
m.directory.metaFile = tmpMT;
return ret;
};
before(setUpSqlDB);
after(sqlHelper.clearDB);
describe('Saved search', () => {
beforeEach(setUpSqlDB);
afterEach(sqlHelper.clearDB);
it('should add album', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
await am.addSavedSearch('Test Album', Utils.clone(query));
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
id: 1,
name: 'Test Album',
searchQuery: query
}]);
});
it('should delete album', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Test Album', Utils.clone(query));
await am.addSavedSearch('Test Album2', Utils.clone(query));
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([
{
id: 1,
name: 'Test Album',
searchQuery: query
},
{
id: 2,
name: 'Test Album2',
searchQuery: query
}]);
await am.deleteAlbum(1);
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
id: 2,
name: 'Test Album2',
searchQuery: query
}]);
await am.deleteAlbum(2);
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
});
});
it('should list album', async () => {
const am = new AlbumManager();
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Test Album', Utils.clone(query));
expect(await am.getAlbums()).to.deep.equalInAnyOrder(([{
id: 1,
name: 'Test Album',
searchQuery: query,
preview: toAlbumPreview(p)
}]));
});
});