1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

implementing duplicates frontend

This commit is contained in:
Patrik J. Braun 2019-01-18 00:26:20 +01:00
parent 849a081cba
commit 41dc64f805
16 changed files with 244 additions and 12 deletions

View File

@ -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;
}

View File

@ -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<number>;
getPossibleDuplicates(): Promise<MediaEntity[]>;
getPossibleDuplicates(): Promise<DuplicatesDTO[]>;
}

View File

@ -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,

View File

@ -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

View File

@ -0,0 +1,5 @@
import {MediaDTO} from './MediaDTO';
export interface DuplicatesDTO {
media: MediaDTO[];
}

View File

@ -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,

View File

@ -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

View File

@ -0,0 +1,15 @@
.same-data {
font-weight: bold;
}
.card{
margin: 8px 0;
}
.row{
margin: 5px 0;
cursor: pointer;
}
.row:hover{
background-color: #f8f9fa;
}

View File

@ -0,0 +1,32 @@
<app-frame>
<div body class="container">
<ng-template [ngIf]="_duplicateService.duplicates.value">
<div *ngFor="let pairs of _duplicateService.duplicates.value" class="card">
<div class="card-body">
<div *ngFor="let media of pairs.media"
class="row"
[routerLink]="['/gallery', getDirectoryPath(media)]"
[queryParams]="queryService.getParams()">
<app-duplicates-photo class="col-1" [media]="media"></app-duplicates-photo>
<div class="col-5">
/{{getDirectoryPath(media)}}/<span class="same-data">{{media.name}}</span>
</div>
<div class="col-2">
<span class="same-data">{{media.metadata.fileSize | fileSize}}</span>
</div>
<div class="col-2" [title]="media.metadata.creationDate">
{{media.metadata.creationDate | date}}
</div>
<div class="col-2">
{{media.metadata.size.width}}x{{media.metadata.size.height}}
</div>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="!_duplicateService.duplicates.value">
loading
</ng-template>
</div>
</app-frame>

View File

@ -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);
}
}

View File

@ -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<DuplicatesDTO[]>;
constructor(private networkService: NetworkService) {
this.duplicates = new BehaviorSubject<DuplicatesDTO[]>(null);
}
public async getDuplicates() {
this.duplicates.next(await this.networkService.getJson<DuplicatesDTO[]>('/admin/duplicates'));
}
}

View File

@ -0,0 +1,13 @@
.icon {
height: 30px;
}
.big-icon {
height: 60px;
}
.photo-container {
width: inherit;
height: inherit;
overflow: hidden;
}

View File

@ -0,0 +1,15 @@
<div class="photo-container">
<ng-template #popTemplate>
<img alt="{{media.name}}"
class="big-icon"
[src]="thumbnail.Src | fixOrientation:Orientation | async"
*ngIf="thumbnail.Available">
</ng-template>
<img alt="{{media.name}}"
class="icon"
[popover]="popTemplate"
triggers="mouseenter:mouseleave"
[src]="thumbnail.Src | fixOrientation:Orientation | async"
*ngIf="thumbnail.Available">
</div>

View File

@ -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 (<PhotoDTO>this.media).metadata.orientation || OrientationTypes.TOP_LEFT;
}
ngOnInit() {
this.thumbnail = this.thumbnailService.getIcon(new MediaIcon(this.media));
}
ngOnDestroy() {
this.thumbnail.destroy();
}
}

View File

@ -35,6 +35,12 @@
class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-basic">
<ng-content select="[navbar-menu]"></ng-content>
<li role="menuitem" *ngIf="isAdmin()">
<a class="dropdown-item" href="#" [routerLink]="['/duplicates']">
<span class="oi oi-layers"></span>
<ng-container i18n>duplicates</ng-container>
</a>
</li>
<li role="menuitem" *ngIf="isAdmin()">
<a class="dropdown-item" href="#" [routerLink]="['/admin']">
<span class="oi oi-wrench"></span>