Add a neat paginator

This commit is contained in:
rubikscraft 2022-05-10 15:03:51 +02:00
parent a5aac2da1d
commit fd24e29c0c
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
14 changed files with 306 additions and 126 deletions

View file

@ -10,7 +10,8 @@
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
"style": "scss",
"skipTests": true
},
"@schematics/angular:application": {
"strict": true

View file

@ -37,23 +37,24 @@ export class MasonryComponent implements AfterViewInit, OnDestroy {
@AutoUnsubscribe()
private subscribeContent() {
return this.content.changes.subscribe(
(items: QueryList<MasonryItemDirective>) => {
const sizes = items.map((i) => i.getSize());
this.handleContentChange(this.content);
return this.content.changes.subscribe(this.handleContentChange.bind(this));
}
if (this.sizesSubscription) {
this.sizesSubscription.unsubscribe();
}
private handleContentChange(items: QueryList<MasonryItemDirective>) {
const sizes = items.map((i) => i.getSize());
this.sizesSubscription = combineLatest(sizes)
.pipe(Throttle(this.update_speed))
.subscribe((output) => {
this.resortItems(items);
});
if (this.sizesSubscription) {
this.sizesSubscription.unsubscribe();
}
this.sizesSubscription = combineLatest(sizes)
.pipe(Throttle(this.update_speed))
.subscribe((output) => {
this.resortItems(items);
}
);
});
this.resortItems(items);
}
private resortItems(items: QueryList<MasonryItemDirective>) {
@ -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) {

View file

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

View file

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

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

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

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

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

View file

@ -1,49 +1,38 @@
<masonry [columns]="columns">
<div *ngFor="let image of images" class="m-2" masonry-item>
<mat-card>
<mat-card-header>
<mat-card-title>Image by you</mat-card-title>
<mat-card-subtitle>
Uploaded {{ image.created | amTimeAgo }}
</mat-card-subtitle>
</mat-card-header>
<picsur-img
mat-card-image
[src]="getThumbnailUrl(image)"
alt="Image uploaded by you"
>
</picsur-img>
<mat-card-actions>
<button mat-stroked-button (click)="viewImage(image)">View</button>
<button mat-button color="warn">Delete</button>
</mat-card-actions>
</mat-card>
</div>
</masonry>
<mat-progress-spinner *ngIf="images === null"></mat-progress-spinner>
<!-- <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> -->
<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>
<mat-card-header>
<mat-card-title>Image by you</mat-card-title>
<mat-card-subtitle>
Uploaded {{ image.created | amTimeAgo }}
</mat-card-subtitle>
</mat-card-header>
<picsur-img
mat-card-image
[src]="getThumbnailUrl(image)"
alt="Image uploaded by you"
>
</picsur-img>
<mat-card-actions>
<button mat-stroked-button (click)="viewImage(image)">View</button>
<button mat-button color="warn" (click)="deleteImage(image)">
Delete
</button>
</mat-card-actions>
</mat-card>
</div>
</masonry>
<paginator
[total-pages]="pages"
[page]="page"
(page)="gotoPage($event)"
></paginator>
</ng-container>

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ const routes: PRoutes = [
{
path: '',
pathMatch: 'full',
redirectTo: '0',
redirectTo: '1',
},
{
path: ':page',

View file

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