From 41dc64f80505e63e3d9c979652dd4ff6c4732eac Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 18 Jan 2019 00:26:20 +0100 Subject: [PATCH] implementing duplicates frontend --- backend/model/sql/GalleryManager.ts | 49 ++++++++++++++++++- backend/model/sql/IGalleryManager.ts | 4 +- backend/routes/GalleryRouter.ts | 12 +++++ backend/routes/PublicRouter.ts | 2 +- backend/{tsconfigX.jsonX => tsconfig.json} | 0 common/entities/DuplicatesDTO.ts | 5 ++ frontend/app/app.module.ts | 16 +++--- frontend/app/app.routing.ts | 5 ++ .../app/duplicates/duplicates.component.css | 15 ++++++ .../app/duplicates/duplicates.component.html | 32 ++++++++++++ .../app/duplicates/duplicates.component.ts | 22 +++++++++ frontend/app/duplicates/duplicates.service.ts | 20 ++++++++ .../photo/photo.duplicates.component.css | 13 +++++ .../photo/photo.duplicates.component.html | 15 ++++++ .../photo/photo.duplicates.component.ts | 40 +++++++++++++++ frontend/app/frame/frame.component.html | 6 +++ 16 files changed, 244 insertions(+), 12 deletions(-) rename backend/{tsconfigX.jsonX => tsconfig.json} (100%) create mode 100644 common/entities/DuplicatesDTO.ts create mode 100644 frontend/app/duplicates/duplicates.component.css create mode 100644 frontend/app/duplicates/duplicates.component.html create mode 100644 frontend/app/duplicates/duplicates.component.ts create mode 100644 frontend/app/duplicates/duplicates.service.ts create mode 100644 frontend/app/duplicates/photo/photo.duplicates.component.css create mode 100644 frontend/app/duplicates/photo/photo.duplicates.component.html create mode 100644 frontend/app/duplicates/photo/photo.duplicates.component.ts diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index 7d2dae52..f3e9bb64 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -18,6 +18,7 @@ import {DiskMangerWorker} from '../threading/DiskMangerWorker'; import {Logger} from '../../Logger'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {ObjectManagerRepository} from '../ObjectManagerRepository'; +import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; const LOG_TAG = '[GalleryManager]'; @@ -173,7 +174,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { const connection = await SQLConnection.getConnection(); const mediaRepository = connection.getRepository(MediaEntity); - const duplicates = await mediaRepository.createQueryBuilder('media') + let duplicates = await mediaRepository.createQueryBuilder('media') .innerJoin(query => query.from(MediaEntity, 'innerMedia') .select(['innerMedia.name as name', 'innerMedia.metadata.fileSize as fileSize', 'count(*)']) .groupBy('innerMedia.name, innerMedia.metadata.fileSize') @@ -181,7 +182,51 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { 'innerMedia', 'media.name=innerMedia.name AND media.metadata.fileSize = innerMedia.fileSize') .innerJoinAndSelect('media.directory', 'directory').getMany(); - return duplicates; + + const duplicateParis: DuplicatesDTO[] = []; + let i = duplicates.length - 1; + while (i >= 0) { + const list = [duplicates[i]]; + let j = i - 1; + while (j >= 0 && duplicates[i].name === duplicates[j].name && duplicates[i].metadata.fileSize === duplicates[j].metadata.fileSize) { + list.push(duplicates[j]); + j--; + } + i = j; + duplicateParis.push({media: list}); + } + + + duplicates = await mediaRepository.createQueryBuilder('media') + .innerJoin(query => query.from(MediaEntity, 'innerMedia') + .select(['innerMedia.metadata.creationDate as creationDate', 'innerMedia.metadata.fileSize as fileSize', 'count(*)']) + .groupBy('innerMedia.name, innerMedia.metadata.fileSize') + .having('count(*)>1'), + 'innerMedia', + 'media.metadata.creationDate=innerMedia.creationDate AND media.metadata.fileSize = innerMedia.fileSize') + .innerJoinAndSelect('media.directory', 'directory').getMany(); + + i = duplicates.length - 1; + while (i >= 0) { + const list = [duplicates[i]]; + let j = i - 1; + while (j >= 0 && duplicates[i].metadata.creationDate === duplicates[j].metadata.creationDate && duplicates[i].metadata.fileSize === duplicates[j].metadata.fileSize) { + list.push(duplicates[j]); + j--; + } + i = j; + if (list.filter(paired => + !!duplicateParis.find(dp => + !!dp.media.find(m => + m.id === paired.id))).length === list.length) { + continue; + } + + duplicateParis.push({media: list}); + } + + + return duplicateParis; } diff --git a/backend/model/sql/IGalleryManager.ts b/backend/model/sql/IGalleryManager.ts index 5f01f269..5dd5fa89 100644 --- a/backend/model/sql/IGalleryManager.ts +++ b/backend/model/sql/IGalleryManager.ts @@ -1,6 +1,6 @@ import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; import {IGalleryManager} from '../interfaces/IGalleryManager'; -import {MediaEntity} from './enitites/MediaEntity'; +import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; export interface ISQLGalleryManager extends IGalleryManager { listDirectory(relativeDirectoryName: string, @@ -15,5 +15,5 @@ export interface ISQLGalleryManager extends IGalleryManager { countMediaSize(): Promise; - getPossibleDuplicates(): Promise; + getPossibleDuplicates(): Promise; } diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index fdb4c066..96f023a1 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -10,6 +10,7 @@ export class GalleryRouter { public static route(app: Express) { this.addGetImageIcon(app); + this.addGetVideoIcon(app); this.addGetImageThumbnail(app); this.addGetVideoThumbnail(app); this.addGetImage(app); @@ -92,6 +93,17 @@ export class GalleryRouter { ); } + + private static addGetVideoIcon(app: Express) { + app.get('/api/gallery/content/:mediaPath(*\.(mp4|ogg|ogv|webm))/icon', + AuthenticationMWs.authenticate, + // TODO: authorize path + GalleryMWs.loadFile, + ThumbnailGeneratorMWs.generateIconFactory(ThumbnailSourceType.Video), + RenderingMWs.renderFile + ); + } + private static addGetImageIcon(app: Express) { app.get('/api/gallery/content/:mediaPath(*\.(jpg|jpeg|jpe|webp|png|gif|svg))/icon', AuthenticationMWs.authenticate, diff --git a/backend/routes/PublicRouter.ts b/backend/routes/PublicRouter.ts index bc54b917..9ac45955 100644 --- a/backend/routes/PublicRouter.ts +++ b/backend/routes/PublicRouter.ts @@ -79,7 +79,7 @@ export class PublicRouter { }); - app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/search*'], + app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/search*'], AuthenticationMWs.tryAuthenticate, setLocale, renderIndex diff --git a/backend/tsconfigX.jsonX b/backend/tsconfig.json similarity index 100% rename from backend/tsconfigX.jsonX rename to backend/tsconfig.json diff --git a/common/entities/DuplicatesDTO.ts b/common/entities/DuplicatesDTO.ts new file mode 100644 index 00000000..66dd7695 --- /dev/null +++ b/common/entities/DuplicatesDTO.ts @@ -0,0 +1,5 @@ +import {MediaDTO} from './MediaDTO'; + +export interface DuplicatesDTO { + media: MediaDTO[]; +} diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index 45694f80..afe0b6b9 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -1,10 +1,4 @@ -import { - Injectable, - LOCALE_ID, - NgModule, - TRANSLATIONS, - TRANSLATIONS_FORMAT -} from '@angular/core'; +import {Injectable, LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT} from '@angular/core'; import {BrowserModule, HAMMER_GESTURE_CONFIG, HammerGestureConfig} from '@angular/platform-browser'; import {FormsModule} from '@angular/forms'; import {AppComponent} from './app.component'; @@ -52,6 +46,7 @@ import {MapSettingsComponent} from './settings/map/map.settings.component'; import {TooltipModule} from 'ngx-bootstrap/tooltip'; import {BsDropdownModule} from 'ngx-bootstrap/dropdown'; import {CollapseModule} from 'ngx-bootstrap/collapse'; +import {PopoverModule} from 'ngx-bootstrap/popover'; import {ThumbnailSettingsComponent} from './settings/thumbnail/thumbanil.settings.component'; import {SearchSettingsComponent} from './settings/search/search.settings.component'; import {SettingsService} from './settings/settings.service'; @@ -75,6 +70,9 @@ import {MapService} from './gallery/map/map.service'; import {MetaFileSettingsComponent} from './settings/metafiles/metafile.settings.component'; import {ThumbnailLoaderService} from './gallery/thumbnailLoader.service'; import {FileSizePipe} from './pipes/FileSizePipe'; +import {DuplicateService} from './duplicates/duplicates.service'; +import {DuplicateComponent} from './duplicates/duplicates.component'; +import {DuplicatesPhotoComponent} from './duplicates/photo/photo.duplicates.component'; @Injectable() @@ -124,6 +122,7 @@ export function translationsFactory(locale: string) { ToastrModule.forRoot(), ModalModule.forRoot(), CollapseModule.forRoot(), + PopoverModule.forRoot(), BsDropdownModule.forRoot(), SlimLoadingBarModule.forRoot(), BsDatepickerModule.forRoot(), @@ -165,6 +164,8 @@ export function translationsFactory(locale: string) { BasicSettingsComponent, OtherSettingsComponent, IndexingSettingsComponent, + DuplicateComponent, + DuplicatesPhotoComponent, StringifyRole, IconizeSortingMethod, StringifySortingMethod, @@ -190,6 +191,7 @@ export function translationsFactory(locale: string) { SettingsService, OverlayService, QueryService, + DuplicateService, { provide: TRANSLATIONS, useFactory: translationsFactory, diff --git a/frontend/app/app.routing.ts b/frontend/app/app.routing.ts index 9f039cb4..6155b8ea 100644 --- a/frontend/app/app.routing.ts +++ b/frontend/app/app.routing.ts @@ -5,6 +5,7 @@ import {GalleryComponent} from './gallery/gallery.component'; import {AdminComponent} from './admin/admin.component'; import {ShareLoginComponent} from './sharelogin/share-login.component'; import {QueryParams} from '../../common/QueryParams'; +import {DuplicateComponent} from './duplicates/duplicates.component'; export function galleryMatcherFunction( segments: UrlSegment[]): UrlMatchResult | null { @@ -50,6 +51,10 @@ const ROUTES: Routes = [ path: 'admin', component: AdminComponent }, + { + path: 'duplicates', + component: DuplicateComponent + }, { matcher: galleryMatcherFunction, component: GalleryComponent diff --git a/frontend/app/duplicates/duplicates.component.css b/frontend/app/duplicates/duplicates.component.css new file mode 100644 index 00000000..fb4eddba --- /dev/null +++ b/frontend/app/duplicates/duplicates.component.css @@ -0,0 +1,15 @@ +.same-data { + font-weight: bold; +} +.card{ + margin: 8px 0; +} + +.row{ + margin: 5px 0; + cursor: pointer; +} + +.row:hover{ + background-color: #f8f9fa; +} diff --git a/frontend/app/duplicates/duplicates.component.html b/frontend/app/duplicates/duplicates.component.html new file mode 100644 index 00000000..7059a32f --- /dev/null +++ b/frontend/app/duplicates/duplicates.component.html @@ -0,0 +1,32 @@ + + +
+ +
+
+
+ +
+ /{{getDirectoryPath(media)}}/{{media.name}} +
+
+ {{media.metadata.fileSize | fileSize}} +
+
+ {{media.metadata.creationDate | date}} +
+
+ {{media.metadata.size.width}}x{{media.metadata.size.height}} +
+
+
+
+
+ + loading + +
+
diff --git a/frontend/app/duplicates/duplicates.component.ts b/frontend/app/duplicates/duplicates.component.ts new file mode 100644 index 00000000..8e7018c0 --- /dev/null +++ b/frontend/app/duplicates/duplicates.component.ts @@ -0,0 +1,22 @@ +import {Component} from '@angular/core'; +import {DuplicateService} from './duplicates.service'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; +import {Utils} from '../../../common/Utils'; +import {QueryService} from '../model/query.service'; + +@Component({ + selector: 'app-duplicate', + templateUrl: './duplicates.component.html', + styleUrls: ['./duplicates.component.css'] +}) +export class DuplicateComponent { + constructor(public _duplicateService: DuplicateService, + public queryService: QueryService) { + this._duplicateService.getDuplicates().catch(console.error); + } + + getDirectoryPath(media: MediaDTO) { + return Utils.concatUrls(media.directory.path, media.directory.name); + } +} + diff --git a/frontend/app/duplicates/duplicates.service.ts b/frontend/app/duplicates/duplicates.service.ts new file mode 100644 index 00000000..75436894 --- /dev/null +++ b/frontend/app/duplicates/duplicates.service.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; +import {NetworkService} from '../model/network/network.service'; +import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; +import {BehaviorSubject} from 'rxjs'; + + +@Injectable() +export class DuplicateService { + + public duplicates: BehaviorSubject; + + constructor(private networkService: NetworkService) { + this.duplicates = new BehaviorSubject(null); + } + + public async getDuplicates() { + this.duplicates.next(await this.networkService.getJson('/admin/duplicates')); + } + +} diff --git a/frontend/app/duplicates/photo/photo.duplicates.component.css b/frontend/app/duplicates/photo/photo.duplicates.component.css new file mode 100644 index 00000000..b2996799 --- /dev/null +++ b/frontend/app/duplicates/photo/photo.duplicates.component.css @@ -0,0 +1,13 @@ +.icon { + height: 30px; +} + +.big-icon { + height: 60px; +} + +.photo-container { + width: inherit; + height: inherit; + overflow: hidden; +} diff --git a/frontend/app/duplicates/photo/photo.duplicates.component.html b/frontend/app/duplicates/photo/photo.duplicates.component.html new file mode 100644 index 00000000..f6091df4 --- /dev/null +++ b/frontend/app/duplicates/photo/photo.duplicates.component.html @@ -0,0 +1,15 @@ +
+ + {{media.name}} + + + {{media.name}} +
diff --git a/frontend/app/duplicates/photo/photo.duplicates.component.ts b/frontend/app/duplicates/photo/photo.duplicates.component.ts new file mode 100644 index 00000000..6d0abe97 --- /dev/null +++ b/frontend/app/duplicates/photo/photo.duplicates.component.ts @@ -0,0 +1,40 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {MediaDTO} from '../../../../common/entities/MediaDTO'; +import {Media} from '../../gallery/Media'; +import {IconThumbnail, Thumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service'; +import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; +import {OrientationTypes} from 'ts-exif-parser'; +import {MediaIcon} from '../../gallery/MediaIcon'; + +@Component({ + selector: 'app-duplicates-photo', + templateUrl: './photo.duplicates.component.html', + styleUrls: ['./photo.duplicates.component.css'] +}) +export class DuplicatesPhotoComponent implements OnInit, OnDestroy { + @Input() media: MediaDTO; + + thumbnail: IconThumbnail; + + + constructor(private thumbnailService: ThumbnailManagerService) { + } + + get Orientation() { + if (!this.media) { + return OrientationTypes.TOP_LEFT; + } + return (this.media).metadata.orientation || OrientationTypes.TOP_LEFT; + } + + ngOnInit() { + this.thumbnail = this.thumbnailService.getIcon(new MediaIcon(this.media)); + + } + + ngOnDestroy() { + this.thumbnail.destroy(); + } + +} + diff --git a/frontend/app/frame/frame.component.html b/frontend/app/frame/frame.component.html index 1ad69de9..da009bd3 100644 --- a/frontend/app/frame/frame.component.html +++ b/frontend/app/frame/frame.component.html @@ -35,6 +35,12 @@ class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-basic"> +
  • + + + duplicates + +