diff --git a/package.json b/package.json
index 4dfd309..a3ddfef 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
},
"dependencies": {
"@angular/animations": "19.2.1",
+ "@angular/cdk": "^19.2.19",
"@angular/common": "19.2.1",
"@angular/compiler": "19.2.1",
"@angular/core": "19.2.1",
diff --git a/src-angular/app/app.module.ts b/src-angular/app/app.module.ts
index 8511388..986553e 100644
--- a/src-angular/app/app.module.ts
+++ b/src-angular/app/app.module.ts
@@ -1,3 +1,5 @@
+import { ScrollingModule } from '@angular/cdk/scrolling'
+import { CommonModule } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -38,9 +40,11 @@ import { RemoveStyleTagsPipe } from './core/pipes/remove-style-tags.pipe'
],
bootstrap: [AppComponent], imports: [
BrowserModule,
+ CommonModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
+ ScrollingModule,
], providers: [provideHttpClient(withInterceptorsFromDi())],
})
export class AppModule { }
diff --git a/src-angular/app/components/browse/result-table/result-table.component.html b/src-angular/app/components/browse/result-table/result-table.component.html
index 01485aa..10d8154 100644
--- a/src-angular/app/components/browse/result-table/result-table.component.html
+++ b/src-angular/app/components/browse/result-table/result-table.component.html
@@ -1,51 +1,53 @@
-
diff --git a/src-angular/app/components/browse/result-table/result-table.component.ts b/src-angular/app/components/browse/result-table/result-table.component.ts
index 763ff3e..0d2cde1 100644
--- a/src-angular/app/components/browse/result-table/result-table.component.ts
+++ b/src-angular/app/components/browse/result-table/result-table.component.ts
@@ -1,29 +1,32 @@
-import { Component, ElementRef, EventEmitter, HostBinding, HostListener, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core'
+import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'
+import { Component, EventEmitter, HostBinding, HostListener, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { Router } from '@angular/router'
+import { Subscription } from 'rxjs'
import { SettingsService } from 'src-angular/app/core/services/settings.service'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { SearchService } from '../../../core/services/search.service'
import { SelectionService } from '../../../core/services/selection.service'
-import { ResultTableRowComponent } from './result-table-row/result-table-row.component'
@Component({
selector: 'app-result-table',
templateUrl: './result-table.component.html',
standalone: false,
})
-export class ResultTableComponent implements OnInit {
+export class ResultTableComponent implements OnInit, OnDestroy {
@HostBinding('class.contents') contents = true
@Output() rowClicked = new EventEmitter()
- @ViewChild('resultTableDiv', { static: true }) resultTableDiv: ElementRef
- @ViewChildren('tableRow') tableRows: QueryList
+ @ViewChild('viewport', { static: true }) viewport: CdkVirtualScrollViewport
activeSong: ChartData[] | null = null
sortDirection: 'asc' | 'desc' = 'asc'
sortColumn: 'name' | 'artist' | 'album' | 'genre' | 'year' | 'charter' | 'length' | 'modifiedTime' | null = null
+ isLoadingMore = false
+ songs: ChartData[][] = []
+ subscription: Subscription[] = []
constructor(
public searchService: SearchService,
@@ -33,18 +36,48 @@ export class ResultTableComponent implements OnInit {
) { }
ngOnInit() {
- this.searchService.newSearch.subscribe(() => {
- this.resultTableDiv.nativeElement.scrollTop = 0
- this.activeSong = null
- setTimeout(() => this.tableScrolled(), 0)
- })
- this.searchService.updateSearch.subscribe(() => {
- setTimeout(() => this.tableScrolled(), 0)
- })
+ this.subscription.push(
+ this.searchService.newSearch.subscribe(() => {
+ if (this.viewport) {
+ this.viewport.scrollToIndex(0)
+ }
+ this.activeSong = null
+ this.isLoadingMore = false
+ this.songs = [...this.searchService.groupedSongs]
+ })
+ )
+
+ this.subscription.push(
+ this.searchService.updateSearch.subscribe(() => {
+ this.isLoadingMore = false
+ this.songs = [...this.searchService.groupedSongs]
+ })
+ )
+ }
+
+ onViewportScroll(): void {
+ if (!this.viewport || this.router.url !== '/browse' || this.isLoadingMore) {
+ return
+ }
+
+ const viewportElement = this.viewport.elementRef.nativeElement
+ const scrollTop = viewportElement.scrollTop
+ const scrollHeight = viewportElement.scrollHeight
+ const clientHeight = viewportElement.clientHeight
+ const threshold = 100
+
+ if (scrollHeight - (scrollTop + clientHeight) < threshold) {
+ this.isLoadingMore = true
+ this.searchService.getNextSearchPage()
+ }
+ }
+
+ trackByFn(_: number, song: ChartData[]): number {
+ return song[0].groupId
}
- get songs() {
- return this.searchService.groupedSongs
+ get tableRowHeight(): number {
+ return this.settingsService.isCompactTable ? 32 : 48
}
hasColumn(column: string) {
@@ -86,15 +119,23 @@ export class ResultTableComponent implements OnInit {
}
}
+ public get tableHeaderPosition(): string {
+ if (!this.viewport) {
+ return '-0px'
+ }
+ const offset = this.viewport.getOffsetToRenderedContentStart()
+
+ return `-${offset}px`
+ }
+
@HostListener('window:resize', ['$event'])
onResize() {
- this.tableScrolled()
- }
- tableScrolled(): void {
- const table = this.resultTableDiv.nativeElement
- if (this.router.url === '/browse' && table.scrollHeight - (table.scrollTop + table.clientHeight) < 100) {
- // Scrolled near the bottom of the table
- this.searchService.getNextSearchPage()
+ if (this.viewport) {
+ this.viewport.checkViewportSize()
}
}
+
+ ngOnDestroy(): void {
+ this.subscription.forEach(sub => sub.unsubscribe())
+ }
}
diff --git a/src-angular/app/core/services/selection.service.ts b/src-angular/app/core/services/selection.service.ts
index 67d2f69..4979b23 100644
--- a/src-angular/app/core/services/selection.service.ts
+++ b/src-angular/app/core/services/selection.service.ts
@@ -12,7 +12,7 @@ export class SelectionService {
public selections: { [groupId: number]: boolean | undefined } = {}
- constructor(searchService: SearchService) {
+ constructor(private searchService: SearchService) {
searchService.newSearch.subscribe(() => {
this.selections = {}
this.deselectAll()
@@ -33,8 +33,8 @@ export class SelectionService {
selectAll() {
this.allSelected = true
- for (const groupId in this.selections) {
- this.selections[groupId] = true
+ for (const song of this.searchService.groupedSongs) {
+ this.selections[song[0].groupId] = true
}
this.selectAllChangedEmitter.emit(true)
}