mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
Implementing Album UI #45
This commit is contained in:
parent
6a08cc1c1c
commit
1e8ec4e96e
@ -15,7 +15,7 @@ export class AlbumRouter {
|
|||||||
|
|
||||||
|
|
||||||
private static addListAlbums(app: Express): void {
|
private static addListAlbums(app: Express): void {
|
||||||
app.get(['/api/album'],
|
app.get(['/api/albums'],
|
||||||
// common part
|
// common part
|
||||||
AuthenticationMWs.authenticate,
|
AuthenticationMWs.authenticate,
|
||||||
AuthenticationMWs.authorise(UserRoles.User),
|
AuthenticationMWs.authorise(UserRoles.User),
|
||||||
@ -28,7 +28,7 @@ export class AlbumRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static addDeleteAlbum(app: Express): void {
|
private static addDeleteAlbum(app: Express): void {
|
||||||
app.delete(['/api/album/:id'],
|
app.delete(['/api/albums/:id'],
|
||||||
// common part
|
// common part
|
||||||
AuthenticationMWs.authenticate,
|
AuthenticationMWs.authenticate,
|
||||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||||
@ -41,7 +41,7 @@ export class AlbumRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static addAddSavedSearch(app: Express): void {
|
private static addAddSavedSearch(app: Express): void {
|
||||||
app.put(['/api/album/saved-search'],
|
app.put(['/api/albums/saved-searches'],
|
||||||
// common part
|
// common part
|
||||||
AuthenticationMWs.authenticate,
|
AuthenticationMWs.authenticate,
|
||||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||||
|
@ -108,7 +108,7 @@ export class PublicRouter {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/search*'],
|
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/albums', '/search*'],
|
||||||
AuthenticationMWs.tryAuthenticate,
|
AuthenticationMWs.tryAuthenticate,
|
||||||
setLocale,
|
setLocale,
|
||||||
renderIndex
|
renderIndex
|
||||||
|
@ -1 +1 @@
|
|||||||
export const DataStructureVersion = 21;
|
export const DataStructureVersion = 22;
|
||||||
|
@ -100,6 +100,9 @@ import {AppRoutingModule} from './app.routing';
|
|||||||
import {CookieService} from 'ngx-cookie-service';
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||||
import {icon, Marker} from 'leaflet';
|
import {icon, Marker} from 'leaflet';
|
||||||
|
import {AlbumsComponent} from './ui/albums/albums.component';
|
||||||
|
import {AlbumComponent} from './ui/albums/album/album.component';
|
||||||
|
import {AlbumsService} from './ui/albums/albums.service';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -178,6 +181,9 @@ Marker.prototype.options.icon = iconDefault;
|
|||||||
LanguageComponent,
|
LanguageComponent,
|
||||||
TimeStampDatePickerComponent,
|
TimeStampDatePickerComponent,
|
||||||
TimeStampTimePickerComponent,
|
TimeStampTimePickerComponent,
|
||||||
|
// Albums
|
||||||
|
AlbumsComponent,
|
||||||
|
AlbumComponent,
|
||||||
// Gallery
|
// Gallery
|
||||||
GalleryLightboxMediaComponent,
|
GalleryLightboxMediaComponent,
|
||||||
GalleryPhotoLoadingComponent,
|
GalleryPhotoLoadingComponent,
|
||||||
@ -241,6 +247,7 @@ Marker.prototype.options.icon = iconDefault;
|
|||||||
NetworkService,
|
NetworkService,
|
||||||
ShareService,
|
ShareService,
|
||||||
UserService,
|
UserService,
|
||||||
|
AlbumsService,
|
||||||
GalleryCacheService,
|
GalleryCacheService,
|
||||||
GalleryService,
|
GalleryService,
|
||||||
MapService,
|
MapService,
|
||||||
|
@ -8,6 +8,7 @@ import {QueryParams} from '../../common/QueryParams';
|
|||||||
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
|
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
|
||||||
import {FacesComponent} from './ui/faces/faces.component';
|
import {FacesComponent} from './ui/faces/faces.component';
|
||||||
import {AuthGuard} from './model/network/helper/auth.guard';
|
import {AuthGuard} from './model/network/helper/auth.guard';
|
||||||
|
import {AlbumsComponent} from './ui/albums/albums.component';
|
||||||
|
|
||||||
export function galleryMatcherFunction(
|
export function galleryMatcherFunction(
|
||||||
segments: UrlSegment[]): UrlMatchResult | null {
|
segments: UrlSegment[]): UrlMatchResult | null {
|
||||||
@ -59,6 +60,11 @@ const routes: Routes = [
|
|||||||
component: DuplicateComponent,
|
component: DuplicateComponent,
|
||||||
canActivate: [AuthGuard]
|
canActivate: [AuthGuard]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'albums',
|
||||||
|
component: AlbumsComponent,
|
||||||
|
canActivate: [AuthGuard]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'faces',
|
path: 'faces',
|
||||||
component: FacesComponent,
|
component: FacesComponent,
|
||||||
|
79
src/frontend/app/ui/albums/album/album.component.css
Normal file
79
src/frontend/app/ui/albums/album/album.component.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.star {
|
||||||
|
margin: 2px;
|
||||||
|
color: #888;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.favourite {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .05s ease-in-out;
|
||||||
|
transform: scale(1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.clickable:hover {
|
||||||
|
transform: scale(1.4, 1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-container {
|
||||||
|
border: 2px solid #333;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
background-color: #bbbbbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image {
|
||||||
|
position: absolute;
|
||||||
|
color: #7f7f7f;
|
||||||
|
font-size: 80px;
|
||||||
|
top: calc(50% - 40px);
|
||||||
|
left: calc(50% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
font-size: medium;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover .info {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover .photo-container {
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
display: inline-block;
|
||||||
|
width: 180px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
28
src/frontend/app/ui/albums/album/album.component.html
Normal file
28
src/frontend/app/ui/albums/album/album.component.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<a [routerLink]="RouterLink"
|
||||||
|
style="display: inline-block;">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="photo-container"
|
||||||
|
[style.width.px]="size"
|
||||||
|
[style.height.px]="size">
|
||||||
|
|
||||||
|
<div class="photo"
|
||||||
|
*ngIf="thumbnail && thumbnail.Available"
|
||||||
|
[style.background-image]="getSanitizedThUrl()"></div>
|
||||||
|
|
||||||
|
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"
|
||||||
|
aria-hidden="true">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--Info box -->
|
||||||
|
<div class="info">
|
||||||
|
{{album.name}}
|
||||||
|
<span *ngIf="CanUpdate"
|
||||||
|
(click)="deleteAlbum($event)"
|
||||||
|
class="star oi oi-remove"></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
77
src/frontend/app/ui/albums/album/album.component.ts
Normal file
77
src/frontend/app/ui/albums/album/album.component.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||||
|
import {Thumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service';
|
||||||
|
import {AuthenticationService} from '../../../model/network/authentication.service';
|
||||||
|
import {AlbumsService} from '../albums.service';
|
||||||
|
import {AlbumBaseDTO} from '../../../../../common/entities/album/AlbumBaseDTO';
|
||||||
|
import {Media} from '../../gallery/Media';
|
||||||
|
import {SavedSearchDTO} from '../../../../../common/entities/album/SavedSearchDTO';
|
||||||
|
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-album',
|
||||||
|
templateUrl: './album.component.html',
|
||||||
|
styleUrls: ['./album.component.css'],
|
||||||
|
providers: [RouterLink],
|
||||||
|
})
|
||||||
|
export class AlbumComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() album: AlbumBaseDTO;
|
||||||
|
@Input() size: number;
|
||||||
|
|
||||||
|
public thumbnail: Thumbnail = null;
|
||||||
|
|
||||||
|
constructor(private thumbnailService: ThumbnailManagerService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private albumService: AlbumsService,
|
||||||
|
public authenticationService: AuthenticationService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get IsSavedSearch(): boolean {
|
||||||
|
return this.album && !!this.AsSavedSearch.searchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
get AsSavedSearch(): SavedSearchDTO {
|
||||||
|
return this.album as SavedSearchDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
get CanUpdate(): boolean {
|
||||||
|
return this.authenticationService.user.getValue().role >= UserRoles.Admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get RouterLink(): any[] {
|
||||||
|
if (this.IsSavedSearch) {
|
||||||
|
return ['/search', this.AsSavedSearch.searchQuery];
|
||||||
|
}
|
||||||
|
// TODO: add nomral albums here once they are ready
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.album.preview) {
|
||||||
|
this.thumbnail = this.thumbnailService.getThumbnail(new Media(this.album.preview, this.size, this.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getSanitizedThUrl(): SafeStyle {
|
||||||
|
return this.sanitizer.bypassSecurityTrustStyle('url(' + this.thumbnail.Src
|
||||||
|
.replace(/\(/g, '%28')
|
||||||
|
.replace(/'/g, '%27')
|
||||||
|
.replace(/\)/g, '%29') + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.thumbnail != null) {
|
||||||
|
this.thumbnail.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAlbum($event: MouseEvent): Promise<void> {
|
||||||
|
$event.preventDefault();
|
||||||
|
$event.stopPropagation();
|
||||||
|
await this.albumService.deleteAlbum(this.album).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
13
src/frontend/app/ui/albums/albums.component.css
Normal file
13
src/frontend/app/ui/albums/albums.component.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
app-album {
|
||||||
|
margin: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-item-msg{
|
||||||
|
height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-face-msg h2{
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
16
src/frontend/app/ui/albums/albums.component.html
Normal file
16
src/frontend/app/ui/albums/albums.component.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<app-frame>
|
||||||
|
|
||||||
|
<div body #container class="container-fluid">
|
||||||
|
<app-album *ngFor="let album of albumsService.albums | async"
|
||||||
|
[album]="album"
|
||||||
|
[size]="size"></app-album>
|
||||||
|
|
||||||
|
<div class="d-flex no-item-msg"
|
||||||
|
*ngIf="(albumsService.albums | async) && (albumsService.albums | async).length == 0">
|
||||||
|
<div class="flex-fill">
|
||||||
|
<h2>:( <ng-container i18n>No albums to show.</ng-container>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-frame>
|
32
src/frontend/app/ui/albums/albums.component.ts
Normal file
32
src/frontend/app/ui/albums/albums.component.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {AlbumsService} from './albums.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-albums',
|
||||||
|
templateUrl: './albums.component.html',
|
||||||
|
styleUrls: ['./albums.component.css']
|
||||||
|
})
|
||||||
|
export class AlbumsComponent implements OnInit {
|
||||||
|
@ViewChild('container', {static: true}) container: ElementRef;
|
||||||
|
public size: number;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(public albumsService: AlbumsService) {
|
||||||
|
this.albumsService.getAlbums().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSize(): void {
|
||||||
|
const size = 220 + 5;
|
||||||
|
// body - container margin
|
||||||
|
const containerWidth = this.container.nativeElement.clientWidth - 30;
|
||||||
|
this.size = (containerWidth / Math.round((containerWidth / size))) - 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
25
src/frontend/app/ui/albums/albums.service.ts
Normal file
25
src/frontend/app/ui/albums/albums.service.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {NetworkService} from '../../model/network/network.service';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AlbumsService {
|
||||||
|
public albums: BehaviorSubject<AlbumBaseDTO[]>;
|
||||||
|
|
||||||
|
constructor(private networkService: NetworkService) {
|
||||||
|
this.albums = new BehaviorSubject<AlbumBaseDTO[]>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async getAlbums(): Promise<void> {
|
||||||
|
this.albums.next((await this.networkService.getJson<AlbumBaseDTO[]>('/albums'))
|
||||||
|
.sort((a, b): number => a.name.localeCompare(b.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAlbum(album: AlbumBaseDTO): Promise<void> {
|
||||||
|
await this.networkService.deleteJson('/albums/' + album.id);
|
||||||
|
await this.getAlbums();
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,9 @@
|
|||||||
[routerLink]="['/gallery']"
|
[routerLink]="['/gallery']"
|
||||||
[queryParams]="queryService.getParams()" [class.active]="isLinkActive('/gallery')" i18n>Gallery</a>
|
[queryParams]="queryService.getParams()" [class.active]="isLinkActive('/gallery')" i18n>Gallery</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" *ngIf="isAlbumsAvailable()">
|
||||||
|
<a class="nav-link" [routerLink]="['/albums']" [class.active]="isLinkActive('/albums')" i18n>Albums</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" *ngIf="isFacesAvailable()">
|
<li class="nav-item" *ngIf="isFacesAvailable()">
|
||||||
<a class="nav-link" [routerLink]="['/faces']" [class.active]="isLinkActive('/faces')" i18n>Faces</a>
|
<a class="nav-link" [routerLink]="['/faces']" [class.active]="isLinkActive('/faces')" i18n>Faces</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -45,5 +45,8 @@ export class FrameComponent {
|
|||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAlbumsAvailable(): boolean {
|
||||||
|
return Config.Client.Album.enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,15 +59,5 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
calcSize() {
|
|
||||||
if (this.size == null || PageHelper.isScrollYVisible()) {
|
|
||||||
const size = 220 + 5;
|
|
||||||
const containerWidth = this.container.nativeElement.parentElement.parentElement.clientWidth;
|
|
||||||
this.size = containerWidth / Math.round((containerWidth / size));
|
|
||||||
}
|
|
||||||
return Math.floor(this.size - 5);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user