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 {
|
||||
app.get(['/api/album'],
|
||||
app.get(['/api/albums'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
@ -28,7 +28,7 @@ export class AlbumRouter {
|
||||
}
|
||||
|
||||
private static addDeleteAlbum(app: Express): void {
|
||||
app.delete(['/api/album/:id'],
|
||||
app.delete(['/api/albums/:id'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||
@ -41,7 +41,7 @@ export class AlbumRouter {
|
||||
}
|
||||
|
||||
private static addAddSavedSearch(app: Express): void {
|
||||
app.put(['/api/album/saved-search'],
|
||||
app.put(['/api/albums/saved-searches'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
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,
|
||||
setLocale,
|
||||
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 {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||
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()
|
||||
@ -178,6 +181,9 @@ Marker.prototype.options.icon = iconDefault;
|
||||
LanguageComponent,
|
||||
TimeStampDatePickerComponent,
|
||||
TimeStampTimePickerComponent,
|
||||
// Albums
|
||||
AlbumsComponent,
|
||||
AlbumComponent,
|
||||
// Gallery
|
||||
GalleryLightboxMediaComponent,
|
||||
GalleryPhotoLoadingComponent,
|
||||
@ -241,6 +247,7 @@ Marker.prototype.options.icon = iconDefault;
|
||||
NetworkService,
|
||||
ShareService,
|
||||
UserService,
|
||||
AlbumsService,
|
||||
GalleryCacheService,
|
||||
GalleryService,
|
||||
MapService,
|
||||
|
@ -8,6 +8,7 @@ import {QueryParams} from '../../common/QueryParams';
|
||||
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
|
||||
import {FacesComponent} from './ui/faces/faces.component';
|
||||
import {AuthGuard} from './model/network/helper/auth.guard';
|
||||
import {AlbumsComponent} from './ui/albums/albums.component';
|
||||
|
||||
export function galleryMatcherFunction(
|
||||
segments: UrlSegment[]): UrlMatchResult | null {
|
||||
@ -59,6 +60,11 @@ const routes: Routes = [
|
||||
component: DuplicateComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'albums',
|
||||
component: AlbumsComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'faces',
|
||||
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']"
|
||||
[queryParams]="queryService.getParams()" [class.active]="isLinkActive('/gallery')" i18n>Gallery</a>
|
||||
</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()">
|
||||
<a class="nav-link" [routerLink]="['/faces']" [class.active]="isLinkActive('/faces')" i18n>Faces</a>
|
||||
</li>
|
||||
|
@ -45,5 +45,8 @@ export class FrameComponent {
|
||||
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