mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-11-28 08:58:49 +02:00
Implementing adv. search query builder #58
This commit is contained in:
parent
de9c58fd90
commit
88015cc33e
@ -4,7 +4,6 @@ import {DiskMangerWorker} from '../src/backend/model/threading/DiskMangerWorker'
|
||||
import {IndexingManager} from '../src/backend/model/database/sql/IndexingManager';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import {SearchTypes} from '../src/common/entities/AutoCompleteItem';
|
||||
import {Utils} from '../src/common/Utils';
|
||||
import {DirectoryDTO} from '../src/common/entities/DirectoryDTO';
|
||||
import {ServerConfig} from '../src/common/config/private/PrivateConfig';
|
||||
@ -21,6 +20,7 @@ import {GalleryRouter} from '../src/backend/routes/GalleryRouter';
|
||||
import {Express} from 'express';
|
||||
import {PersonRouter} from '../src/backend/routes/PersonRouter';
|
||||
import {QueryParams} from '../src/common/QueryParams';
|
||||
import {SearchQueryTypes, TextSearch} from '../src/common/entities/SearchQueryDTO';
|
||||
|
||||
|
||||
export interface BenchmarkResult {
|
||||
@ -48,10 +48,6 @@ class BMGalleryRouter extends GalleryRouter {
|
||||
GalleryRouter.addSearch(app);
|
||||
}
|
||||
|
||||
public static addInstantSearch(app: Express) {
|
||||
GalleryRouter.addInstantSearch(app);
|
||||
}
|
||||
|
||||
public static addAutoComplete(app: Express) {
|
||||
GalleryRouter.addAutoComplete(app);
|
||||
}
|
||||
@ -128,16 +124,15 @@ export class BenchmarkRunner {
|
||||
return await bm.run(this.RUNS);
|
||||
}
|
||||
|
||||
async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchTypes }[]> {
|
||||
async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchQueryTypes }[]> {
|
||||
await this.setupDB();
|
||||
const types = Utils.enumToArray(SearchTypes).map(a => a.key).concat([null]);
|
||||
const results: { result: BenchmarkResult, searchType: SearchTypes }[] = [];
|
||||
const types = Utils.enumToArray(SearchQueryTypes).map(a => a.key).concat([null]);
|
||||
const results: { result: BenchmarkResult, searchType: SearchQueryTypes }[] = [];
|
||||
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
const req = Utils.clone(this.requestTemplate);
|
||||
req.params.text = text;
|
||||
req.query[QueryParams.gallery.search.type] = types[i];
|
||||
const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchTypes[types[i]] : 'any') + '`', req);
|
||||
req.query[QueryParams.gallery.search.query] = <TextSearch>{type: types[i], text: text};
|
||||
const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchQueryTypes[types[i]] : 'any') + '`', req);
|
||||
BMGalleryRouter.addSearch(bm.BmExpressApp);
|
||||
|
||||
results.push({result: await bm.run(this.RUNS), searchType: types[i]});
|
||||
@ -145,14 +140,6 @@ export class BenchmarkRunner {
|
||||
return results;
|
||||
}
|
||||
|
||||
async bmInstantSearch(text: string): Promise<BenchmarkResult> {
|
||||
await this.setupDB();
|
||||
const req = Utils.clone(this.requestTemplate);
|
||||
req.params.text = text;
|
||||
const bm = new Benchmark('Instant search for `' + text + '`', req);
|
||||
BMGalleryRouter.addInstantSearch(bm.BmExpressApp);
|
||||
return await bm.run(this.RUNS);
|
||||
}
|
||||
|
||||
async bmAutocomplete(text: string): Promise<BenchmarkResult> {
|
||||
await this.setupDB();
|
||||
|
@ -71,7 +71,6 @@ const run = async () => {
|
||||
printResult(await bm.bmListDirectory());
|
||||
printResult(await bm.bmListPersons());
|
||||
(await bm.bmAllSearch('a')).forEach(res => printResult(res.result));
|
||||
printResult(await bm.bmInstantSearch('a'));
|
||||
printResult(await bm.bmAutocomplete('a'));
|
||||
printLine('*Measurements run ' + RUNS + ' times and an average was calculated.');
|
||||
console.log(resultsText);
|
||||
|
@ -162,7 +162,7 @@ export class GalleryMWs {
|
||||
return next();
|
||||
}
|
||||
|
||||
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.type];
|
||||
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.query];
|
||||
|
||||
try {
|
||||
const result = await ObjectManagers.getInstance().SearchManager.search(query);
|
||||
@ -203,7 +203,7 @@ export class GalleryMWs {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.type];
|
||||
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.query];
|
||||
|
||||
const photo = await ObjectManagers.getInstance()
|
||||
.SearchManager.getRandomPhoto(query);
|
||||
|
@ -2,8 +2,13 @@ import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
|
||||
export class SearchManager implements ISearchManager {
|
||||
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
SearchQueryTypes,
|
||||
SomeOfSearchQuery,
|
||||
TextSearch,
|
||||
TextSearchQueryTypes
|
||||
TextSearchQueryMatchTypes
|
||||
} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {GalleryManager} from './GalleryManager';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
@ -359,7 +359,7 @@ export class SearchManager implements ISearchManager {
|
||||
return new Brackets((q: WhereExpression) => {
|
||||
|
||||
const createMatchString = (str: string) => {
|
||||
return (<TextSearch>query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`;
|
||||
return (<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match ? str : `%${str}%`;
|
||||
};
|
||||
|
||||
const LIKE = (<TextSearch>query).negate ? 'NOT LIKE' : 'LIKE';
|
||||
@ -416,7 +416,7 @@ export class SearchManager implements ISearchManager {
|
||||
// Matching for array type fields
|
||||
const matchArrayField = (fieldName: string) => {
|
||||
q[whereFN](new Brackets(qbr => {
|
||||
if ((<TextSearch>query).matchType !== TextSearchQueryTypes.exact_match) {
|
||||
if ((<TextSearch>query).matchType !== TextSearchQueryMatchTypes.exact_match) {
|
||||
qbr[whereFN](`${fieldName} ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
} else {
|
||||
|
@ -16,7 +16,6 @@ export const QueryParams = {
|
||||
photo: 'p',
|
||||
sharingKey_query: 'sk',
|
||||
sharingKey_params: 'sharingKey',
|
||||
searchText: 'searchText',
|
||||
directory: 'directory',
|
||||
knownLastModified: 'klm',
|
||||
knownLastScanned: 'kls'
|
||||
|
@ -12,15 +12,48 @@ export enum SearchQueryTypes {
|
||||
|
||||
// TEXT search types
|
||||
any_text = 100,
|
||||
person,
|
||||
keyword,
|
||||
position,
|
||||
caption,
|
||||
file_name,
|
||||
directory,
|
||||
file_name,
|
||||
keyword,
|
||||
person,
|
||||
position,
|
||||
}
|
||||
|
||||
export enum TextSearchQueryTypes {
|
||||
export const ListSearchQueryTypes = [
|
||||
SearchQueryTypes.AND,
|
||||
SearchQueryTypes.OR,
|
||||
SearchQueryTypes.SOME_OF,
|
||||
];
|
||||
export const TextSearchQueryTypes = [
|
||||
SearchQueryTypes.any_text,
|
||||
SearchQueryTypes.caption,
|
||||
SearchQueryTypes.directory,
|
||||
SearchQueryTypes.file_name,
|
||||
SearchQueryTypes.keyword,
|
||||
SearchQueryTypes.person,
|
||||
SearchQueryTypes.position,
|
||||
];
|
||||
|
||||
export const MetadataSearchQueryTypes = [
|
||||
// non-text metadata
|
||||
SearchQueryTypes.date,
|
||||
SearchQueryTypes.rating,
|
||||
SearchQueryTypes.distance,
|
||||
SearchQueryTypes.resolution,
|
||||
SearchQueryTypes.orientation,
|
||||
|
||||
// TEXT search types
|
||||
SearchQueryTypes.any_text,
|
||||
SearchQueryTypes.caption,
|
||||
SearchQueryTypes.directory,
|
||||
SearchQueryTypes.file_name,
|
||||
SearchQueryTypes.keyword,
|
||||
SearchQueryTypes.person,
|
||||
SearchQueryTypes.position,
|
||||
];
|
||||
|
||||
export enum TextSearchQueryMatchTypes {
|
||||
exact_match = 1, like = 2
|
||||
}
|
||||
|
||||
@ -104,7 +137,7 @@ export interface TextSearch extends NegatableSearchQuery {
|
||||
SearchQueryTypes.caption |
|
||||
SearchQueryTypes.file_name |
|
||||
SearchQueryTypes.directory;
|
||||
matchType: TextSearchQueryTypes;
|
||||
matchType: TextSearchQueryMatchTypes;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,7 @@ import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.
|
||||
import {ErrorInterceptor} from './model/network/helper/error.interceptor';
|
||||
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
|
||||
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
|
||||
import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -174,6 +175,7 @@ export function translationsFactory(locale: string) {
|
||||
GalleryMapLightboxComponent,
|
||||
FrameComponent,
|
||||
GallerySearchComponent,
|
||||
GallerySearchQueryEntryComponent,
|
||||
GalleryShareComponent,
|
||||
GalleryNavigatorComponent,
|
||||
GalleryPhotoComponent,
|
||||
|
@ -27,7 +27,7 @@ export function galleryMatcherFunction(
|
||||
}
|
||||
if (path === 'search') {
|
||||
if (segments.length > 1) {
|
||||
posParams[QueryParams.gallery.searchText] = segments[1];
|
||||
posParams[QueryParams.gallery.search.query] = segments[1];
|
||||
}
|
||||
return {consumed: segments.slice(0, Math.min(segments.length, 2)), posParams};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<a [routerLink]="['/search', person.name, {type: SearchTypes[SearchTypes.person]}]"
|
||||
<a [routerLink]="['/search', searchQuery]"
|
||||
style="display: inline-block;">
|
||||
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {PersonDTO} from '../../../../../common/entities/PersonDTO';
|
||||
import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {PersonThumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service';
|
||||
import {FacesService} from '../faces.service';
|
||||
import {AuthenticationService} from '../../../model/network/authentication.service';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../common/entities/SearchQueryDTO';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
|
||||
@Component({
|
||||
selector: 'app-face',
|
||||
@ -19,7 +20,7 @@ export class FaceComponent implements OnInit, OnDestroy {
|
||||
@Input() size: number;
|
||||
|
||||
thumbnail: PersonThumbnail = null;
|
||||
SearchTypes = SearchTypes;
|
||||
public searchQuery: any;
|
||||
|
||||
constructor(private thumbnailService: ThumbnailManagerService,
|
||||
private _sanitizer: DomSanitizer,
|
||||
@ -34,6 +35,12 @@ export class FaceComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.thumbnail = this.thumbnailService.getPersonThumbnail(this.person);
|
||||
this.searchQuery = {};
|
||||
this.searchQuery[QueryParams.gallery.search.query] = <TextSearch>{
|
||||
type: SearchQueryTypes.person,
|
||||
text: this.person.name,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,12 @@ import {Injectable} from '@angular/core';
|
||||
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {AutoCompleteItem, SearchTypes} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {SortingMethods} from '../../../../common/entities/SortingMethods';
|
||||
import {VersionService} from '../../model/version.service';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
interface CacheItem<T> {
|
||||
timestamp: number;
|
||||
@ -21,7 +22,6 @@ export class GalleryCacheService {
|
||||
private static readonly INSTANT_SEARCH_PREFIX = 'instant_search:';
|
||||
private static readonly SEARCH_PREFIX = 'search:';
|
||||
private static readonly SORTING_PREFIX = 'sorting:';
|
||||
private static readonly SEARCH_TYPE_PREFIX = ':type:';
|
||||
private static readonly VERSION = 'version';
|
||||
|
||||
constructor(private versionService: VersionService) {
|
||||
@ -40,7 +40,7 @@ export class GalleryCacheService {
|
||||
const tmp = localStorage.getItem(key);
|
||||
if (tmp != null) {
|
||||
const value: CacheItem<SearchResultDTO> = JSON.parse(tmp);
|
||||
if (value.timestamp < Date.now() - Config.Client.Search.instantSearchCacheTimeout) {
|
||||
if (value.timestamp < Date.now() - Config.Client.Search.searchCacheTimeout) {
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
@ -158,19 +158,15 @@ export class GalleryCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
public getSearch(text: string, type?: SearchTypes): SearchResultDTO {
|
||||
public getSearch(query: SearchQueryDTO): SearchResultDTO {
|
||||
if (Config.Client.Other.enableCache === false) {
|
||||
return null;
|
||||
}
|
||||
let key = GalleryCacheService.SEARCH_PREFIX + text;
|
||||
if (typeof type !== 'undefined' && type !== null) {
|
||||
key += GalleryCacheService.SEARCH_TYPE_PREFIX + type;
|
||||
}
|
||||
|
||||
const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query);
|
||||
return GalleryCacheService.loadCacheItem(key);
|
||||
}
|
||||
|
||||
public setSearch(text: string, type: SearchTypes, searchResult: SearchResultDTO): void {
|
||||
public setSearch(query: SearchQueryDTO, searchResult: SearchResultDTO): void {
|
||||
if (Config.Client.Other.enableCache === false) {
|
||||
return;
|
||||
}
|
||||
@ -178,10 +174,7 @@ export class GalleryCacheService {
|
||||
timestamp: Date.now(),
|
||||
item: searchResult
|
||||
};
|
||||
let key = GalleryCacheService.SEARCH_PREFIX + text;
|
||||
if (typeof type !== 'undefined' && type !== null) {
|
||||
key += GalleryCacheService.SEARCH_TYPE_PREFIX + type;
|
||||
}
|
||||
const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query);
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(tmp));
|
||||
} catch (e) {
|
||||
|
@ -3,7 +3,6 @@ import {AuthenticationService} from '../../model/network/authentication.service'
|
||||
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||
import {GalleryService} from './gallery.service';
|
||||
import {GalleryGridComponent} from './grid/grid.gallery.component';
|
||||
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
@ -18,7 +17,7 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {SeededRandomService} from '../../model/seededRandom.service';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {FileDTO} from '../../../../common/entities/FileDTO';
|
||||
import {SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery',
|
||||
@ -37,7 +36,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
public isPhotoWithLocation = false;
|
||||
public countDown: { day: number, hour: number, minute: number, second: number } = null;
|
||||
public readonly mapEnabled: boolean;
|
||||
readonly SearchTypes: typeof SearchTypes;
|
||||
readonly SearchTypes: typeof SearchQueryTypes;
|
||||
private $counter: Observable<number>;
|
||||
private subscription: { [key: string]: Subscription } = {
|
||||
content: null,
|
||||
@ -54,7 +53,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
private _navigation: NavigationService,
|
||||
private rndService: SeededRandomService) {
|
||||
this.mapEnabled = Config.Client.Map.enabled;
|
||||
this.SearchTypes = SearchTypes;
|
||||
this.SearchTypes = SearchQueryTypes;
|
||||
PageHelper.showScrollY();
|
||||
}
|
||||
|
||||
@ -115,15 +114,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private onRoute = async (params: Params) => {
|
||||
const searchText = params[QueryParams.gallery.searchText];
|
||||
if (searchText && searchText !== '') {
|
||||
const typeString: string = params[QueryParams.gallery.search.type];
|
||||
let type: SearchTypes = null;
|
||||
if (typeString && typeString !== '') {
|
||||
type = <any>SearchTypes[<any>typeString];
|
||||
}
|
||||
const searchQuery = params[QueryParams.gallery.search.query];
|
||||
if (searchQuery) {
|
||||
|
||||
this._galleryService.search(searchText, type).catch(console.error);
|
||||
this._galleryService.search(searchQuery).catch(console.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import {Injectable} from '@angular/core';
|
||||
import {NetworkService} from '../../model/network/network.service';
|
||||
import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
|
||||
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {GalleryCacheService} from './cache.gallery.service';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
@ -11,6 +10,7 @@ import {NavigationService} from '../../model/navigation.service';
|
||||
import {SortingMethods} from '../../../../common/entities/SortingMethods';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {PG2ConfMap} from '../../../../common/PG2ConfMap';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -23,15 +23,7 @@ export class GalleryService {
|
||||
};
|
||||
private lastDirectory: DirectoryDTO;
|
||||
private searchId: any;
|
||||
private ongoingSearch: {
|
||||
text: string,
|
||||
type: SearchTypes
|
||||
} = null;
|
||||
private ongoingInstantSearch: {
|
||||
text: string,
|
||||
type: SearchTypes
|
||||
} = null;
|
||||
private runInstantSearchFor: string;
|
||||
private ongoingSearch: SearchQueryDTO = null;
|
||||
|
||||
constructor(private networkService: NetworkService,
|
||||
private galleryCacheService: GalleryCacheService,
|
||||
@ -124,99 +116,35 @@ export class GalleryService {
|
||||
}
|
||||
}
|
||||
|
||||
public async search(text: string, type?: SearchTypes): Promise<void> {
|
||||
public async search(query: SearchQueryDTO): Promise<void> {
|
||||
if (this.searchId != null) {
|
||||
clearTimeout(this.searchId);
|
||||
}
|
||||
if (text === null || text === '' || text.trim() === '.') {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.ongoingSearch = {text: text, type: type};
|
||||
this.ongoingSearch = query;
|
||||
|
||||
|
||||
this.setContent(new ContentWrapper());
|
||||
const cw = new ContentWrapper();
|
||||
cw.searchResult = this.galleryCacheService.getSearch(text, type);
|
||||
cw.searchResult = this.galleryCacheService.getSearch(query);
|
||||
if (cw.searchResult == null) {
|
||||
if (this.runInstantSearchFor === text && !type) {
|
||||
await this.instantSearch(text, type);
|
||||
return;
|
||||
}
|
||||
const params: { [key: string]: any } = {};
|
||||
if (typeof type !== 'undefined' && type !== null) {
|
||||
params[QueryParams.gallery.search.type] = type;
|
||||
}
|
||||
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search/' + text, params)).searchResult;
|
||||
if (this.ongoingSearch &&
|
||||
(this.ongoingSearch.text !== text || this.ongoingSearch.type !== type)) {
|
||||
return;
|
||||
}
|
||||
this.galleryCacheService.setSearch(text, type, cw.searchResult);
|
||||
params[QueryParams.gallery.search.query] = query;
|
||||
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search', params)).searchResult;
|
||||
this.galleryCacheService.setSearch(query, cw.searchResult);
|
||||
}
|
||||
|
||||
if (this.ongoingSearch !== query) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setContent(cw);
|
||||
}
|
||||
|
||||
public async instantSearch(text: string, type?: SearchTypes): Promise<ContentWrapper> {
|
||||
if (text === null || text === '' || text.trim() === '.') {
|
||||
const content = new ContentWrapper(this.lastDirectory);
|
||||
this.setContent(content);
|
||||
if (this.searchId != null) {
|
||||
clearTimeout(this.searchId);
|
||||
}
|
||||
if (!this.lastDirectory) {
|
||||
this.loadDirectory('/').catch(console.error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.searchId != null) {
|
||||
clearTimeout(this.searchId);
|
||||
}
|
||||
this.runInstantSearchFor = null;
|
||||
this.ongoingInstantSearch = {text: text, type: type};
|
||||
|
||||
|
||||
const cw = new ContentWrapper();
|
||||
cw.directory = null;
|
||||
cw.searchResult = this.galleryCacheService.getSearch(text);
|
||||
if (cw.searchResult == null) {
|
||||
// If result is not search cache, try to load more
|
||||
this.searchId = setTimeout(() => {
|
||||
this.search(text, type).catch(console.error);
|
||||
this.searchId = null;
|
||||
}, Config.Client.Search.InstantSearchTimeout);
|
||||
|
||||
cw.searchResult = this.galleryCacheService.getInstantSearch(text);
|
||||
|
||||
if (cw.searchResult == null) {
|
||||
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/instant-search/' + text)).searchResult;
|
||||
if (this.ongoingInstantSearch &&
|
||||
(this.ongoingInstantSearch.text !== text || this.ongoingInstantSearch.type !== type)) {
|
||||
return;
|
||||
}
|
||||
this.galleryCacheService.setInstantSearch(text, cw.searchResult);
|
||||
}
|
||||
}
|
||||
this.setContent(cw);
|
||||
|
||||
// if instant search do not have a result, do not do a search
|
||||
if (cw.searchResult.media.length === 0 && cw.searchResult.directories.length === 0) {
|
||||
if (this.searchId != null) {
|
||||
clearTimeout(this.searchId);
|
||||
}
|
||||
}
|
||||
return cw;
|
||||
|
||||
}
|
||||
|
||||
|
||||
isSearchResult(): boolean {
|
||||
return !!this.content.value.searchResult;
|
||||
}
|
||||
|
||||
|
||||
runInstantSearch(searchText: string) {
|
||||
this.runInstantSearchFor = searchText;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {Dimension, IRenderable} from '../../../../model/IRenderable';
|
||||
import {GridMedia} from '../GridMedia';
|
||||
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {Thumbnail, ThumbnailManagerService} from '../../thumbnailManager.service';
|
||||
import {Config} from '../../../../../../common/config/public/Config';
|
||||
import {PageHelper} from '../../../../model/page.helper';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../../../../common/entities/PhotoDTO';
|
||||
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-grid-photo',
|
||||
@ -20,11 +20,11 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
@ViewChild('photoContainer', {static: true}) container: ElementRef;
|
||||
|
||||
thumbnail: Thumbnail;
|
||||
keywords: { value: string, type: SearchTypes }[] = null;
|
||||
keywords: { value: string, type: SearchQueryTypes }[] = null;
|
||||
infoBarVisible = false;
|
||||
animationTimer: number = null;
|
||||
|
||||
readonly SearchTypes: typeof SearchTypes = SearchTypes;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes;
|
||||
searchEnabled = true;
|
||||
|
||||
wasInView: boolean = null;
|
||||
@ -60,9 +60,9 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
if (Config.Client.Faces.enabled) {
|
||||
const names: string[] = (metadata.faces || []).map(f => f.name);
|
||||
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
|
||||
.map(n => ({value: n, type: SearchTypes.person}));
|
||||
.map(n => ({value: n, type: SearchQueryTypes.person}));
|
||||
}
|
||||
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
|
||||
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword})));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import {Observable, Subscription, timer} from 'rxjs';
|
||||
import {filter} from 'rxjs/operators';
|
||||
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
|
||||
import {GalleryLightboxMediaComponent} from '../media/media.lightbox.gallery.component';
|
||||
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
|
||||
import {Config} from '../../../../../../common/config/public/Config';
|
||||
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
export enum PlayBackStates {
|
||||
Paused = 1,
|
||||
@ -46,13 +46,13 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
|
||||
public controllersAlwaysOn = false;
|
||||
public controllersVisible = true;
|
||||
public drag = {x: 0, y: 0};
|
||||
public SearchTypes = SearchTypes;
|
||||
public SearchQueryTypes = SearchQueryTypes;
|
||||
private visibilityTimer: number = null;
|
||||
private timer: Observable<number>;
|
||||
private timerSub: Subscription;
|
||||
private prevDrag = {x: 0, y: 0};
|
||||
private prevZoom = 1;
|
||||
private faceContainerDim = {width: 0, height: 0};
|
||||
public faceContainerDim = {width: 0, height: 0};
|
||||
|
||||
constructor(public fullScreenService: FullScreenService) {
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import {VideoDTO, VideoMetadata} from '../../../../../../common/entities/VideoDT
|
||||
import {Utils} from '../../../../../../common/Utils';
|
||||
import {QueryService} from '../../../../model/query.service';
|
||||
import {MapService} from '../../map/map.service';
|
||||
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-info-panel',
|
||||
@ -19,8 +19,8 @@ export class InfoPanelLightboxComponent implements OnInit {
|
||||
|
||||
public readonly mapEnabled: boolean;
|
||||
public readonly searchEnabled: boolean;
|
||||
keywords: { value: string, type: SearchTypes }[] = null;
|
||||
readonly SearchTypes: typeof SearchTypes = SearchTypes;
|
||||
keywords: { value: string, type: SearchQueryTypes }[] = null;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes;
|
||||
|
||||
constructor(public queryService: QueryService,
|
||||
public mapService: MapService) {
|
||||
@ -59,9 +59,9 @@ export class InfoPanelLightboxComponent implements OnInit {
|
||||
if (Config.Client.Faces.enabled) {
|
||||
const names: string[] = (metadata.faces || []).map(f => f.name);
|
||||
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
|
||||
.map(n => ({value: n, type: SearchTypes.person}));
|
||||
.map(n => ({value: n, type: SearchQueryTypes.person}));
|
||||
}
|
||||
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
|
||||
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword})));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {Utils} from '../../../../../common/Utils';
|
||||
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO';
|
||||
import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-navbar',
|
||||
@ -27,7 +27,7 @@ export class GalleryNavigatorComponent implements OnChanges {
|
||||
sortingMethodsType: { key: number; value: string }[] = [];
|
||||
config = Config;
|
||||
DefaultSorting = Config.Client.Other.defaultPhotoSortingMethod;
|
||||
readonly SearchTypes = SearchTypes;
|
||||
readonly SearchQueryTypes = SearchQueryTypes;
|
||||
private readonly RootFolderName: string;
|
||||
|
||||
constructor(private _authService: AuthenticationService,
|
||||
|
@ -0,0 +1,6 @@
|
||||
.query-list{
|
||||
padding-left: 25px;
|
||||
}
|
||||
label{
|
||||
margin-top: 0.3rem;
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
<div class="row mt-1 mb-1" *ngIf="queryEntry">
|
||||
<ng-container *ngIf="IsListQuery">
|
||||
<div class="input-group col-md-2">
|
||||
<select
|
||||
id="listSearchType"
|
||||
name="listSearchType"
|
||||
class="form-control"
|
||||
[(ngModel)]="queryEntry.type"
|
||||
(ngModelChange)="onChangeType($event)">
|
||||
<option *ngFor="let opt of SearchQueryTypesEnum" [ngValue]="opt.key">{{opt.value}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<ng-container *ngIf="queryEntry.type == SearchQueryTypes.SOME_OF">
|
||||
<label class="col-md-4 control-label" for="someOfMinValue">
|
||||
<ng-container i18n>At least this many</ng-container>
|
||||
(1-{{AsListQuery.list.length}}):</label>
|
||||
<input
|
||||
type="number" min="1" [max]="AsListQuery.list.length" class="form-control col-md-2" placeholder="1"
|
||||
title="At least this many"
|
||||
i18n-title
|
||||
[(ngModel)]="AsSomeOfQuery.min"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
name="someOfMinValue"
|
||||
id="someOfMinValue"
|
||||
required="required">
|
||||
<div class="col-md-3"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="queryEntry.type != SearchQueryTypes.SOME_OF">
|
||||
<div class="col-md-9"></div>
|
||||
</ng-container>
|
||||
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
|
||||
class="btn float-right col-md-1">
|
||||
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
|
||||
</button>
|
||||
<div class="container query-list">
|
||||
<app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i"
|
||||
[(ngModel)]="sq"
|
||||
(delete)="itemDeleted(i)">
|
||||
</app-gallery-search-query-entry>
|
||||
</div>
|
||||
<button class="btn btn-primary mx-auto" (click)="addQuery()">
|
||||
<span class="oi oi-plus" aria-hidden="true" aria-label="Add"> Add</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!IsListQuery">
|
||||
<div class="input-group col-md-2">
|
||||
<select
|
||||
id="searchType"
|
||||
name="searchType"
|
||||
class="form-control"
|
||||
[(ngModel)]="queryEntry.type"
|
||||
(ngModelChange)="onChangeType(queryEntry)">
|
||||
<option *ngFor="let opt of SearchQueryTypesEnum" [ngValue]="opt.key">{{opt.value}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group col-md-9" *ngIf="IsTextQuery">
|
||||
<input
|
||||
id="searchField"
|
||||
name="searchField"
|
||||
placeholder="link"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsTextQuery.text"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="text"/>
|
||||
</div>
|
||||
<ng-container [ngSwitch]="queryEntry.type">
|
||||
<div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-9 d-flex">
|
||||
<div class="input-group col-md-4">
|
||||
<input type="number" class="form-control" placeholder="1"
|
||||
id="distance"
|
||||
min="0"
|
||||
step="0.1"
|
||||
[(ngModel)]="AsDistanceQuery.distance"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
name="distance" required>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group col-md-8">
|
||||
<label class="col-md-4 control-label" for="maxResolution">From</label>
|
||||
<input id="from"
|
||||
name="from"
|
||||
title="From"
|
||||
placeholder="New York"
|
||||
i18n-title
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsDistanceQuery.from.text"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="SearchQueryTypes.date" class="col-md-9 d-flex">
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">From:</label>
|
||||
<input id="afterDate"
|
||||
name="afterDate"
|
||||
title="After"
|
||||
i18n-title
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsDateQuery.after"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="date">
|
||||
</div>
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">To:</label>
|
||||
<input id="beforeDate"
|
||||
name="beforeDate"
|
||||
title="Before"
|
||||
i18n-title
|
||||
[(ngModel)]="AsDateQuery.before"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
class="form-control input-md"
|
||||
type="date">
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="SearchQueryTypes.rating" class="col-md-9 d-flex">
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
|
||||
<input id="minRating"
|
||||
name="minRating"
|
||||
title="Minimum Rating"
|
||||
placeholder="0"
|
||||
i18n-title
|
||||
min="0"
|
||||
[max]="AsRatingQuery.max || 5"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsRatingQuery.min"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="number">
|
||||
</div>
|
||||
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
|
||||
<input id="maxRating"
|
||||
name="maxRating"
|
||||
title="Maximum Rating"
|
||||
placeholder="5"
|
||||
i18n-title
|
||||
[min]="AsRatingQuery.min || 0"
|
||||
max="5"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsRatingQuery.max"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="SearchQueryTypes.resolution" class="col-md-9 d-flex">
|
||||
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
|
||||
<input id="minResolution"
|
||||
name="minResolution"
|
||||
title="Minimum Rating"
|
||||
placeholder="0"
|
||||
i18n-title
|
||||
min="0"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsResolutionQuery.min"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="number">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">Mpx</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-md-6">
|
||||
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
|
||||
<input id="maxResolution"
|
||||
name="maxResolution"
|
||||
title="Maximum Rating"
|
||||
placeholder="5"
|
||||
i18n-title
|
||||
[min]="AsResolutionQuery.min || 0"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="AsResolutionQuery.max"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
type="number">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">Mpx</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-9 d-flex">
|
||||
<div class="input-group col-md-6">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
id="orientation"
|
||||
name="orientation"
|
||||
title="Orientation"
|
||||
switch-on-color="primary"
|
||||
switch-off-color="primary"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Portrait"
|
||||
switch-on-text="Landscape"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
(ngModelChange)="onChange(queryEntry)"
|
||||
[(ngModel)]="AsOrientationQuery.landscape">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
|
||||
class="btn float-right col-md-1"
|
||||
(click)="deleteItem()">
|
||||
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,161 @@
|
||||
import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core';
|
||||
import {
|
||||
DateSearch,
|
||||
DistanceSearch,
|
||||
ListSearchQueryTypes,
|
||||
OrientationSearch,
|
||||
RatingSearch,
|
||||
ResolutionSearch,
|
||||
SearchListQuery,
|
||||
SearchQueryDTO,
|
||||
SearchQueryTypes,
|
||||
SomeOfSearchQuery,
|
||||
TextSearch,
|
||||
TextSearchQueryTypes
|
||||
} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
import {Utils} from '../../../../../../common/Utils';
|
||||
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search-query-entry',
|
||||
templateUrl: './query-entry.search.gallery.component.html',
|
||||
styleUrls: ['./query-entry.search.gallery.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => GallerySearchQueryEntryComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => GallerySearchQueryEntryComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class GallerySearchQueryEntryComponent implements ControlValueAccessor, Validator, OnChanges {
|
||||
public queryEntry: SearchQueryDTO;
|
||||
public SearchQueryTypesEnum: { value: string; key: SearchQueryTypes }[];
|
||||
public SearchQueryTypes = SearchQueryTypes;
|
||||
@Output() delete = new EventEmitter<void>();
|
||||
|
||||
constructor() {
|
||||
this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes);
|
||||
|
||||
}
|
||||
|
||||
get IsTextQuery(): boolean {
|
||||
return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type);
|
||||
}
|
||||
|
||||
get IsListQuery(): boolean {
|
||||
return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type);
|
||||
}
|
||||
|
||||
get AsListQuery(): SearchListQuery {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsDateQuery(): DateSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsResolutionQuery(): ResolutionSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsOrientationQuery(): OrientationSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsDistanceQuery(): DistanceSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsRatingQuery(): RatingSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsSomeOfQuery(): SomeOfSearchQuery {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
get AsTextQuery(): TextSearch {
|
||||
return <any>this.queryEntry;
|
||||
}
|
||||
|
||||
validate(control: FormControl): ValidationErrors {
|
||||
return {required: true};
|
||||
}
|
||||
|
||||
addQuery(): void {
|
||||
console.log('clicked', this.IsListQuery);
|
||||
if (!this.IsListQuery) {
|
||||
return;
|
||||
}
|
||||
this.AsListQuery.list.push(<TextSearch>{type: SearchQueryTypes.any_text, text: ''});
|
||||
}
|
||||
|
||||
onChangeType($event: any) {
|
||||
if (this.IsListQuery) {
|
||||
delete this.AsTextQuery.text;
|
||||
this.AsListQuery.list = this.AsListQuery.list || [
|
||||
<TextSearch>{type: SearchQueryTypes.any_text, text: ''},
|
||||
<TextSearch>{type: SearchQueryTypes.any_text, text: ''}
|
||||
];
|
||||
} else {
|
||||
delete this.AsListQuery.list;
|
||||
}
|
||||
if (this.queryEntry.type === SearchQueryTypes.distance) {
|
||||
this.AsDistanceQuery.from = {text: ''};
|
||||
this.AsDistanceQuery.distance = 1;
|
||||
} else {
|
||||
delete this.AsDistanceQuery.from;
|
||||
delete this.AsDistanceQuery.distance;
|
||||
}
|
||||
|
||||
if (this.queryEntry.type === SearchQueryTypes.orientation) {
|
||||
this.AsOrientationQuery.landscape = true;
|
||||
} else {
|
||||
delete this.AsOrientationQuery.landscape;
|
||||
}
|
||||
this.onChange(this.queryEntry);
|
||||
}
|
||||
|
||||
deleteItem() {
|
||||
this.delete.emit();
|
||||
}
|
||||
|
||||
itemDeleted(i: number) {
|
||||
this.AsListQuery.list.splice(i, 1);
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
// console.log('ngOnChanges', this.queryEntry);
|
||||
|
||||
}
|
||||
|
||||
public onChange(value: any): void {
|
||||
// console.log('onChange', this.queryEntry);
|
||||
}
|
||||
|
||||
public onTouched(): void {
|
||||
}
|
||||
|
||||
public writeValue(obj: any): void {
|
||||
this.queryEntry = obj;
|
||||
// console.log('write value', this.queryEntry);
|
||||
this.ngOnChanges();
|
||||
}
|
||||
|
||||
public registerOnChange(fn: any): void {
|
||||
// console.log('registerOnChange', fn);
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
public registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
(keyup)="onSearchChange($event)"
|
||||
(blur)="onFocusLost()"
|
||||
(focus)="onFocus()"
|
||||
[(ngModel)]="searchText"
|
||||
[(ngModel)]="rawSearchText"
|
||||
#name="ngModel"
|
||||
size="30"
|
||||
ngControl="search"
|
||||
@ -19,26 +19,63 @@
|
||||
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0"
|
||||
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
|
||||
<div class="autocomplete-item" *ngFor="let item of autoCompleteItems">
|
||||
<a [routerLink]="['/search', item.text, {type: SearchTypes[item.type]}]">
|
||||
<div>
|
||||
<span [ngSwitch]="item.type">
|
||||
<span *ngSwitchCase="SearchTypes.photo" class="oi oi-image"></span>
|
||||
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
|
||||
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
|
||||
</span>
|
||||
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group-btn" style="display: block">
|
||||
<button class="btn btn-light" type="button"
|
||||
[routerLink]="['/search', searchText]">
|
||||
[routerLink]="['/search', HTMLSearchQuery]">
|
||||
<span class="oi oi-magnifying-glass"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group-btn" style="display: block">
|
||||
<button class="btn btn-light" type="button" (click)="openModal(searchModal)">
|
||||
<span class="oi oi-chevron-bottom"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<ng-template #searchModal>
|
||||
<!-- 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">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
disabled
|
||||
[ngModel]="rawSearchText"
|
||||
size="30"
|
||||
name="srch-term-preview"
|
||||
id="srch-term-preview"
|
||||
autocomplete="off">
|
||||
|
||||
<app-gallery-search-query-entry
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
name="search-root"
|
||||
(delete)="resetQuery()">
|
||||
|
||||
</app-gallery-search-query-entry>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import {Component, OnDestroy} from '@angular/core';
|
||||
import {Component, OnDestroy, TemplateRef} from '@angular/core';
|
||||
import {AutoCompleteService} from './autocomplete.service';
|
||||
import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {ActivatedRoute, Params, RouterLink} from '@angular/router';
|
||||
import {GalleryService} from '../gallery.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {NavigationService} from '../../../model/navigation.service';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search',
|
||||
@ -17,31 +20,48 @@ import {QueryParams} from '../../../../../common/QueryParams';
|
||||
export class GallerySearchComponent implements OnDestroy {
|
||||
|
||||
autoCompleteItems: AutoCompleteRenderItem[] = [];
|
||||
public searchText = '';
|
||||
public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''};
|
||||
mouseOverAutoComplete = false;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes;
|
||||
modalRef: BsModalRef;
|
||||
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
|
||||
private cache = {
|
||||
lastAutocomplete: '',
|
||||
lastInstantSearch: ''
|
||||
};
|
||||
mouseOverAutoComplete = false;
|
||||
|
||||
readonly SearchTypes: typeof SearchTypes;
|
||||
private readonly subscription: Subscription = null;
|
||||
|
||||
constructor(private _autoCompleteService: AutoCompleteService,
|
||||
private _galleryService: GalleryService,
|
||||
private navigationService: NavigationService,
|
||||
private _route: ActivatedRoute) {
|
||||
private _route: ActivatedRoute,
|
||||
private modalService: BsModalService) {
|
||||
|
||||
this.SearchTypes = SearchTypes;
|
||||
this.SearchQueryTypes = SearchQueryTypes;
|
||||
this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map(v => ({key: v, value: SearchQueryTypes[v]}));
|
||||
|
||||
this.subscription = this._route.params.subscribe((params: Params) => {
|
||||
const searchText = params[QueryParams.gallery.searchText];
|
||||
if (searchText && searchText !== '') {
|
||||
this.searchText = searchText;
|
||||
const searchQuery = params[QueryParams.gallery.search.query];
|
||||
if (searchQuery) {
|
||||
this.searchQueryDTO = searchQuery;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get rawSearchText() {
|
||||
return JSON.stringify(this.searchQueryDTO);
|
||||
}
|
||||
|
||||
public set rawSearchText(val: any) {
|
||||
|
||||
}
|
||||
|
||||
get HTMLSearchQuery() {
|
||||
const searchQuery: any = {};
|
||||
searchQuery[QueryParams.gallery.search.query] = this.searchQueryDTO;
|
||||
return searchQuery;
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.subscription !== null) {
|
||||
@ -57,19 +77,9 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
if (Config.Client.Search.AutoComplete.enabled &&
|
||||
this.cache.lastAutocomplete !== searchText) {
|
||||
this.cache.lastAutocomplete = searchText;
|
||||
this.autocomplete(searchText).catch(console.error);
|
||||
// this.autocomplete(searchText).catch(console.error);
|
||||
}
|
||||
|
||||
if (Config.Client.Search.instantSearchEnabled &&
|
||||
this.cache.lastInstantSearch !== searchText) {
|
||||
this.cache.lastInstantSearch = searchText;
|
||||
if (searchText === '') {
|
||||
return this.navigationService.toGallery().catch(console.error);
|
||||
}
|
||||
this._galleryService.runInstantSearch(searchText);
|
||||
this.navigationService.search(searchText).catch(console.error);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -84,9 +94,25 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public onFocus() {
|
||||
this.autocomplete(this.searchText).catch(console.error);
|
||||
// TODO: implement autocomplete
|
||||
// this.autocomplete(this.searchText).catch(console.error);
|
||||
}
|
||||
|
||||
public async openModal(template: TemplateRef<any>) {
|
||||
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
document.body.style.paddingRight = '0px';
|
||||
}
|
||||
|
||||
public hideModal() {
|
||||
this.modalRef.hide();
|
||||
this.modalRef = null;
|
||||
}
|
||||
|
||||
resetQuery() {
|
||||
this.searchQueryDTO = <TextSearch>{text: '', type: SearchQueryTypes.any_text};
|
||||
}
|
||||
|
||||
|
||||
private emptyAutoComplete() {
|
||||
this.autoCompleteItems = [];
|
||||
}
|
||||
@ -126,9 +152,9 @@ class AutoCompleteRenderItem {
|
||||
public preText = '';
|
||||
public highLightText = '';
|
||||
public postText = '';
|
||||
public type: SearchTypes;
|
||||
public type: SearchQueryTypes;
|
||||
|
||||
constructor(public text: string, searchText: string, type: SearchTypes) {
|
||||
constructor(public text: string, searchText: string, type: SearchQueryTypes) {
|
||||
const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase());
|
||||
if (preIndex > -1) {
|
||||
this.preText = text.substring(0, preIndex);
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
SearchQueryTypes,
|
||||
SomeOfSearchQuery,
|
||||
TextSearch,
|
||||
TextSearchQueryTypes
|
||||
TextSearchQueryMatchTypes
|
||||
} from '../../../../../src/common/entities/SearchQueryDTO';
|
||||
import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager';
|
||||
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
@ -482,7 +482,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
query = <TextSearch>{
|
||||
text: 'Boba',
|
||||
type: SearchQueryTypes.any_text,
|
||||
matchType: TextSearchQueryTypes.exact_match
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
};
|
||||
expect(Utils.clone(await sm.search(query)))
|
||||
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
|
||||
@ -496,7 +496,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
query = <TextSearch>{
|
||||
text: 'Boba Fett',
|
||||
type: SearchQueryTypes.any_text,
|
||||
matchType: TextSearchQueryTypes.exact_match
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
};
|
||||
|
||||
expect(Utils.clone(await sm.search(query)))
|
||||
@ -571,7 +571,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: 'star wars',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.keyword
|
||||
};
|
||||
|
||||
@ -585,7 +585,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: 'wookiees',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.keyword
|
||||
};
|
||||
|
||||
@ -684,14 +684,14 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: '/wars dir',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.directory
|
||||
};
|
||||
|
||||
|
||||
expect(Utils.clone(await sm.search(<TextSearch>{
|
||||
text: '/wars dir',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.directory
|
||||
}))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
|
||||
searchQuery: query,
|
||||
@ -704,7 +704,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: '/wars dir/Return of the Jedi',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.directory
|
||||
};
|
||||
|
||||
@ -718,7 +718,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: '/wars dir/Return of the Jedi',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.directory
|
||||
};
|
||||
|
||||
@ -752,7 +752,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
query = <TextSearch>{
|
||||
text: 'Boba',
|
||||
type: SearchQueryTypes.person,
|
||||
matchType: TextSearchQueryTypes.exact_match
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
};
|
||||
|
||||
expect(Utils.clone(await sm.search(query))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
|
||||
@ -766,13 +766,13 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
query = <TextSearch>{
|
||||
text: 'Boba Fett',
|
||||
type: SearchQueryTypes.person,
|
||||
matchType: TextSearchQueryTypes.exact_match
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
};
|
||||
|
||||
expect(Utils.clone(await sm.search(<TextSearch>{
|
||||
text: 'Boba Fett',
|
||||
type: SearchQueryTypes.person,
|
||||
matchType: TextSearchQueryTypes.exact_match
|
||||
matchType: TextSearchQueryMatchTypes.exact_match
|
||||
}))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
|
||||
searchQuery: query,
|
||||
directories: [],
|
||||
@ -1098,7 +1098,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
|
||||
|
||||
query = <TextSearch>{
|
||||
text: 'wookiees',
|
||||
matchType: TextSearchQueryTypes.exact_match,
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.keyword
|
||||
};
|
||||
expect(Utils.clone(await sm.getRandomPhoto(query))).to.deep.equalInAnyOrder(searchifyMedia(p_faceLess));
|
||||
|
Loading…
Reference in New Issue
Block a user