1
0
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:
Patrik J. Braun 2021-05-28 21:01:59 +02:00
parent 6a08cc1c1c
commit 1e8ec4e96e
15 changed files with 294 additions and 15 deletions

View File

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

View File

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

View File

@ -1 +1 @@
export const DataStructureVersion = 21; export const DataStructureVersion = 22;

View File

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

View File

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

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

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

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

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

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

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

View 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();
}
}

View File

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

View File

@ -45,5 +45,8 @@ export class FrameComponent {
this.authService.logout(); this.authService.logout();
} }
isAlbumsAvailable(): boolean {
return Config.Client.Album.enabled;
}
} }

View File

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