Add a neat paginator
This commit is contained in:
parent
a5aac2da1d
commit
fd24e29c0c
|
@ -10,7 +10,8 @@
|
|||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
"style": "scss",
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
|
|
|
@ -37,8 +37,11 @@ export class MasonryComponent implements AfterViewInit, OnDestroy {
|
|||
|
||||
@AutoUnsubscribe()
|
||||
private subscribeContent() {
|
||||
return this.content.changes.subscribe(
|
||||
(items: QueryList<MasonryItemDirective>) => {
|
||||
this.handleContentChange(this.content);
|
||||
return this.content.changes.subscribe(this.handleContentChange.bind(this));
|
||||
}
|
||||
|
||||
private handleContentChange(items: QueryList<MasonryItemDirective>) {
|
||||
const sizes = items.map((i) => i.getSize());
|
||||
|
||||
if (this.sizesSubscription) {
|
||||
|
@ -53,8 +56,6 @@ export class MasonryComponent implements AfterViewInit, OnDestroy {
|
|||
|
||||
this.resortItems(items);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private resortItems(items: QueryList<MasonryItemDirective>) {
|
||||
const itemsArray = items.toArray();
|
||||
|
@ -71,7 +72,7 @@ export class MasonryComponent implements AfterViewInit, OnDestroy {
|
|||
|
||||
let smallestColumn = 0;
|
||||
let smallestColumnSize = columnSizes[0];
|
||||
for (let j = 1; j < columnSizes.length; j++) {
|
||||
for (let j = columnSizes.length - 1; j >= 0; j--) {
|
||||
let better_j = (j + i) % columnSizes.length;
|
||||
|
||||
if (columnSizes[better_j] <= smallestColumnSize) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(1)"
|
||||
*ngIf="showFirstLast"
|
||||
[disabled]="page <= 1"
|
||||
>
|
||||
<mat-icon>first_page</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="changePage(page - 1)" [disabled]="page <= 1">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="firstPagesRange !== null">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(i)"
|
||||
*ngFor="let i of firstPagesRange | range"
|
||||
>
|
||||
<span>{{ i }}</span>
|
||||
</button>
|
||||
|
||||
<div class="paginator-filler">
|
||||
<span>...</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(i)"
|
||||
[class.mat-flat-button]="i === page"
|
||||
*ngFor="let i of pagesRange | range"
|
||||
>
|
||||
<span>{{ i }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="lastPagesRange !== null">
|
||||
<div class="paginator-filler">
|
||||
<span>...</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(i)"
|
||||
*ngFor="let i of lastPagesRange | range"
|
||||
>
|
||||
<span>{{ i }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(page + 1)"
|
||||
[disabled]="page >= totalPages"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changePage(totalPages)"
|
||||
*ngIf="showFirstLast"
|
||||
[disabled]="page >= totalPages"
|
||||
>
|
||||
<mat-icon>last_page</mat-icon>
|
||||
</button>
|
|
@ -0,0 +1,23 @@
|
|||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
& > * {
|
||||
margin-inline: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.paginator-filler {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
85
frontend/src/app/components/paginator/paginator.component.ts
Normal file
85
frontend/src/app/components/paginator/paginator.component.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Required } from 'src/app/models/decorators/required.decorator';
|
||||
|
||||
@Component({
|
||||
selector: 'paginator',
|
||||
templateUrl: './paginator.component.html',
|
||||
styleUrls: ['./paginator.component.scss'],
|
||||
})
|
||||
export class PaginatorComponent implements OnInit {
|
||||
totalPages: number;
|
||||
@Input('total-pages') @Required set totalPagesInput(value: number) {
|
||||
this.totalPages = value;
|
||||
this.calculateRanges();
|
||||
}
|
||||
|
||||
page: number = 1;
|
||||
@Input('page') set pageInput(value: number) {
|
||||
this.page = value;
|
||||
this.calculateRanges();
|
||||
}
|
||||
@Output('page') pageChange = new EventEmitter<number>();
|
||||
|
||||
@Input('show-first-last') showFirstLast: boolean = true;
|
||||
|
||||
@Input('shown-pages') shownPages: number = 7;
|
||||
@Input('shown-first-pages') shownFirstPages: number = 1;
|
||||
@Input('shown-last-pages') shownLastPages: number = 1;
|
||||
|
||||
firstPagesRange: [number, number] | null = null;
|
||||
lastPagesRange: [number, number] | null = null;
|
||||
pagesRange: [number, number] = [0, 0];
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calculateRanges();
|
||||
}
|
||||
|
||||
changePage(page: number) {
|
||||
if (page === this.page) return;
|
||||
|
||||
this.pageChange.emit(page);
|
||||
this.page = page;
|
||||
this.calculateRanges();
|
||||
}
|
||||
|
||||
private calculateRanges() {
|
||||
if (this.totalPages <= this.shownPages) {
|
||||
this.pagesRange = [1, this.totalPages];
|
||||
return;
|
||||
}
|
||||
|
||||
this.pagesRange = [
|
||||
this.page - Math.floor((this.shownPages - 1) / 2),
|
||||
this.page + Math.ceil((this.shownPages - 1) / 2),
|
||||
];
|
||||
|
||||
if (this.pagesRange[0] < 1) {
|
||||
const offset = 1 - this.pagesRange[0];
|
||||
this.pagesRange[0] += offset;
|
||||
this.pagesRange[1] += offset;
|
||||
}
|
||||
|
||||
if (this.pagesRange[1] > this.totalPages) {
|
||||
const offset = this.pagesRange[1] - this.totalPages;
|
||||
this.pagesRange[0] -= offset;
|
||||
this.pagesRange[1] -= offset;
|
||||
}
|
||||
|
||||
if (this.pagesRange[0] > this.shownFirstPages) {
|
||||
this.pagesRange[0] += 1 + this.shownFirstPages;
|
||||
this.firstPagesRange = [1, this.shownFirstPages];
|
||||
} else {
|
||||
this.firstPagesRange = null;
|
||||
}
|
||||
|
||||
const lastPage = this.totalPages - this.shownLastPages + 1;
|
||||
if (this.pagesRange[1] < lastPage) {
|
||||
this.pagesRange[1] -= 1 + this.shownLastPages;
|
||||
this.lastPagesRange = [lastPage, this.totalPages];
|
||||
} else {
|
||||
this.lastPagesRange = null;
|
||||
}
|
||||
}
|
||||
}
|
13
frontend/src/app/components/paginator/paginator.module.ts
Normal file
13
frontend/src/app/components/paginator/paginator.module.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RangeModule } from '../range/range.module';
|
||||
import { PaginatorComponent } from './paginator.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PaginatorComponent],
|
||||
imports: [CommonModule, MatIconModule, MatButtonModule, RangeModule],
|
||||
exports: [PaginatorComponent],
|
||||
})
|
||||
export class PaginatorModule {}
|
10
frontend/src/app/components/range/range.module.ts
Normal file
10
frontend/src/app/components/range/range.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RangePipe } from './range.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RangePipe],
|
||||
imports: [CommonModule],
|
||||
exports: [RangePipe],
|
||||
})
|
||||
export class RangeModule {}
|
25
frontend/src/app/components/range/range.pipe.ts
Normal file
25
frontend/src/app/components/range/range.pipe.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'range',
|
||||
})
|
||||
export class RangePipe implements PipeTransform {
|
||||
transform(length: unknown): number[] {
|
||||
if (typeof length === 'number') {
|
||||
return Array.from({ length }, (_, i) => i);
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(length) &&
|
||||
typeof length[0] === 'number' &&
|
||||
typeof length[1] === 'number'
|
||||
) {
|
||||
return Array.from(
|
||||
{ length: length[1] + 1 - length[0] },
|
||||
(_, i) => i + length[0]
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('Invalid range');
|
||||
}
|
||||
}
|
|
@ -1,3 +1,10 @@
|
|||
<mat-progress-spinner *ngIf="images === null"></mat-progress-spinner>
|
||||
|
||||
<h3 *ngIf="images !== null && images.length <= 0">
|
||||
There are no images to display
|
||||
</h3>
|
||||
|
||||
<ng-container *ngIf="images !== null && images.length > 0">
|
||||
<masonry [columns]="columns">
|
||||
<div *ngFor="let image of images" class="m-2" masonry-item>
|
||||
<mat-card>
|
||||
|
@ -15,35 +22,17 @@
|
|||
</picsur-img>
|
||||
<mat-card-actions>
|
||||
<button mat-stroked-button (click)="viewImage(image)">View</button>
|
||||
<button mat-button color="warn">Delete</button>
|
||||
<button mat-button color="warn" (click)="deleteImage(image)">
|
||||
Delete
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</masonry>
|
||||
|
||||
<!-- <div class="paginator">
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<mat-icon>first_page</mat-icon>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<span>1</span>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<span>2</span>
|
||||
</button>
|
||||
<button class="mat-stroked-button" color="accent" mat-icon-button>
|
||||
<span>3</span>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<span>4</span>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
<button class="mat-stroked-button" mat-icon-button>
|
||||
<mat-icon>last_page</mat-icon>
|
||||
</button>
|
||||
</div> -->
|
||||
<paginator
|
||||
[total-pages]="pages"
|
||||
[page]="page"
|
||||
(page)="gotoPage($event)"
|
||||
></paginator>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
.column-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
margin-inline: .5em;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import {
|
||||
Component, OnInit
|
||||
} from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import { SupportedMime } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
|
@ -23,6 +21,9 @@ export class ImagesComponent implements OnInit {
|
|||
images: EImage[] | null = null;
|
||||
columns = 1;
|
||||
|
||||
page: number = 1;
|
||||
pages: number = 1;
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly router: Router,
|
||||
|
@ -30,19 +31,25 @@ export class ImagesComponent implements OnInit {
|
|||
private readonly imageService: ImageService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
ngOnInit() {
|
||||
this.load().catch(this.logger.error);
|
||||
}
|
||||
|
||||
private async load() {
|
||||
const params = this.route.snapshot.paramMap;
|
||||
|
||||
let page = Number(params.get('id') ?? '');
|
||||
if (isNaN(page)) page = 0;
|
||||
let thispage = Number(params.get('page') ?? '');
|
||||
if (isNaN(thispage) || thispage <= 0) thispage = 1;
|
||||
this.page = thispage;
|
||||
|
||||
this.subscribeMobile();
|
||||
|
||||
const result = await this.imageService.ListMyImages(24, page);
|
||||
const result = await this.imageService.ListMyImages(24, this.page - 1);
|
||||
if (HasFailed(result)) {
|
||||
return this.logger.error(result.getReason());
|
||||
}
|
||||
|
||||
this.pages = result.pages;
|
||||
this.images = result.images;
|
||||
}
|
||||
|
||||
|
@ -68,4 +75,14 @@ export class ImagesComponent implements OnInit {
|
|||
viewImage(image: EImage) {
|
||||
this.router.navigate(['/view', image.id]);
|
||||
}
|
||||
|
||||
deleteImage(image: EImage) {
|
||||
this.images = this.images?.filter((i) => i.id !== image.id) ?? null;
|
||||
}
|
||||
|
||||
gotoPage(page: number) {
|
||||
this.router.navigate(['/images', page]).then(() => {
|
||||
this.load().catch(this.logger.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,27 +2,24 @@ import { CommonModule } from '@angular/common';
|
|||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MomentModule } from 'ngx-moment';
|
||||
import { MasonryModule } from 'src/app/components/masonry/masonry.module';
|
||||
import { PaginatorModule } from 'src/app/components/paginator/paginator.module';
|
||||
import { PicsurImgModule } from 'src/app/components/picsur-img/picsur-img.module';
|
||||
import { ImagesComponent } from './images.component';
|
||||
import { ImagesRoutingModule } from './images.routing.module';
|
||||
import { MasonryPipe } from './masonry.pipe';
|
||||
import { MomentModule } from 'ngx-moment';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ResizeObserverModule } from '@ng-web-apis/resize-observer';
|
||||
import { MasonryModule } from 'src/app/components/masonry/masonry.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ImagesComponent, MasonryPipe],
|
||||
declarations: [ImagesComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ImagesRoutingModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatPaginatorModule,
|
||||
MatIconModule,
|
||||
ResizeObserverModule,
|
||||
MatProgressSpinnerModule,
|
||||
MasonryModule,
|
||||
PaginatorModule,
|
||||
PicsurImgModule,
|
||||
MomentModule,
|
||||
],
|
||||
|
|
|
@ -9,7 +9,7 @@ const routes: PRoutes = [
|
|||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '0',
|
||||
redirectTo: '1',
|
||||
},
|
||||
{
|
||||
path: ':page',
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'masonry',
|
||||
})
|
||||
export class MasonryPipe<T> implements PipeTransform {
|
||||
transform(value: T[], numColumns: number, colNum: number): T[] {
|
||||
if (value.length === 0) return value;
|
||||
|
||||
if (
|
||||
numColumns < 1 ||
|
||||
colNum < 0 ||
|
||||
isNaN(numColumns) ||
|
||||
isNaN(colNum) ||
|
||||
colNum > numColumns
|
||||
) {
|
||||
throw new Error('Invalid column configuration');
|
||||
}
|
||||
|
||||
return value.filter((val, index) => {
|
||||
return index % numColumns === colNum;
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue