diff --git a/frontend/angular.json b/frontend/angular.json index fc1eda7..be833dd 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -10,7 +10,8 @@ "projectType": "application", "schematics": { "@schematics/angular:component": { - "style": "scss" + "style": "scss", + "skipTests": true }, "@schematics/angular:application": { "strict": true diff --git a/frontend/src/app/components/masonry/masonry.component.ts b/frontend/src/app/components/masonry/masonry.component.ts index 953a1c4..52ae802 100644 --- a/frontend/src/app/components/masonry/masonry.component.ts +++ b/frontend/src/app/components/masonry/masonry.component.ts @@ -37,23 +37,24 @@ export class MasonryComponent implements AfterViewInit, OnDestroy { @AutoUnsubscribe() private subscribeContent() { - return this.content.changes.subscribe( - (items: QueryList) => { - 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) { + 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) { @@ -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) { diff --git a/frontend/src/app/components/paginator/paginator.component.html b/frontend/src/app/components/paginator/paginator.component.html new file mode 100644 index 0000000..e00b6b9 --- /dev/null +++ b/frontend/src/app/components/paginator/paginator.component.html @@ -0,0 +1,64 @@ + + + + + + +
+ ... +
+
+ + + + +
+ ... +
+ + +
+ + + diff --git a/frontend/src/app/components/paginator/paginator.component.scss b/frontend/src/app/components/paginator/paginator.component.scss new file mode 100644 index 0000000..929722b --- /dev/null +++ b/frontend/src/app/components/paginator/paginator.component.scss @@ -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; + } +} diff --git a/frontend/src/app/components/paginator/paginator.component.ts b/frontend/src/app/components/paginator/paginator.component.ts new file mode 100644 index 0000000..e27f0bb --- /dev/null +++ b/frontend/src/app/components/paginator/paginator.component.ts @@ -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(); + + @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; + } + } +} diff --git a/frontend/src/app/components/paginator/paginator.module.ts b/frontend/src/app/components/paginator/paginator.module.ts new file mode 100644 index 0000000..da1715a --- /dev/null +++ b/frontend/src/app/components/paginator/paginator.module.ts @@ -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 {} diff --git a/frontend/src/app/components/range/range.module.ts b/frontend/src/app/components/range/range.module.ts new file mode 100644 index 0000000..f0eb706 --- /dev/null +++ b/frontend/src/app/components/range/range.module.ts @@ -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 {} diff --git a/frontend/src/app/components/range/range.pipe.ts b/frontend/src/app/components/range/range.pipe.ts new file mode 100644 index 0000000..49f12ca --- /dev/null +++ b/frontend/src/app/components/range/range.pipe.ts @@ -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'); + } +} diff --git a/frontend/src/app/routes/images/images.component.html b/frontend/src/app/routes/images/images.component.html index 6924587..3746af1 100644 --- a/frontend/src/app/routes/images/images.component.html +++ b/frontend/src/app/routes/images/images.component.html @@ -1,49 +1,38 @@ - -
- - - Image by you - - Uploaded {{ image.created | amTimeAgo }} - - - - - - - - - -
-
+ - +

+ There are no images to display +

+ + + +
+ + + Image by you + + Uploaded {{ image.created | amTimeAgo }} + + + + + + + + + +
+
+ + +
diff --git a/frontend/src/app/routes/images/images.component.scss b/frontend/src/app/routes/images/images.component.scss index d89a5f7..e69de29 100644 --- a/frontend/src/app/routes/images/images.component.scss +++ b/frontend/src/app/routes/images/images.component.scss @@ -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; - } -} diff --git a/frontend/src/app/routes/images/images.component.ts b/frontend/src/app/routes/images/images.component.ts index a6546f2..3e52447 100644 --- a/frontend/src/app/routes/images/images.component.ts +++ b/frontend/src/app/routes/images/images.component.ts @@ -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); + }); + } } diff --git a/frontend/src/app/routes/images/images.module.ts b/frontend/src/app/routes/images/images.module.ts index 86330de..e59a435 100644 --- a/frontend/src/app/routes/images/images.module.ts +++ b/frontend/src/app/routes/images/images.module.ts @@ -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, ], diff --git a/frontend/src/app/routes/images/images.routing.module.ts b/frontend/src/app/routes/images/images.routing.module.ts index c247739..38628f9 100644 --- a/frontend/src/app/routes/images/images.routing.module.ts +++ b/frontend/src/app/routes/images/images.routing.module.ts @@ -9,7 +9,7 @@ const routes: PRoutes = [ { path: '', pathMatch: 'full', - redirectTo: '0', + redirectTo: '1', }, { path: ':page', diff --git a/frontend/src/app/routes/images/masonry.pipe.ts b/frontend/src/app/routes/images/masonry.pipe.ts deleted file mode 100644 index aeb8ce0..0000000 --- a/frontend/src/app/routes/images/masonry.pipe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'masonry', -}) -export class MasonryPipe 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; - }); - } -}