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:
parent
849a081cba
commit
41dc64f805
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
5
common/entities/DuplicatesDTO.ts
Normal file
5
common/entities/DuplicatesDTO.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {MediaDTO} from './MediaDTO';
|
||||
|
||||
export interface DuplicatesDTO {
|
||||
media: MediaDTO[];
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
15
frontend/app/duplicates/duplicates.component.css
Normal file
15
frontend/app/duplicates/duplicates.component.css
Normal 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;
|
||||
}
|
32
frontend/app/duplicates/duplicates.component.html
Normal file
32
frontend/app/duplicates/duplicates.component.html
Normal 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>
|
22
frontend/app/duplicates/duplicates.component.ts
Normal file
22
frontend/app/duplicates/duplicates.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
20
frontend/app/duplicates/duplicates.service.ts
Normal file
20
frontend/app/duplicates/duplicates.service.ts
Normal 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'));
|
||||
}
|
||||
|
||||
}
|
13
frontend/app/duplicates/photo/photo.duplicates.component.css
Normal file
13
frontend/app/duplicates/photo/photo.duplicates.component.css
Normal file
@ -0,0 +1,13 @@
|
||||
.icon {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.big-icon {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
overflow: hidden;
|
||||
}
|
@ -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>
|
40
frontend/app/duplicates/photo/photo.duplicates.component.ts
Normal file
40
frontend/app/duplicates/photo/photo.duplicates.component.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user