[Library] Add Infinite Scrolling

Add infinite scrolling and fix selecting rows
This commit is contained in:
2025-05-04 16:45:21 +02:00
parent 35fd50c728
commit f0453a3daf
9 changed files with 127 additions and 43 deletions

View File

@@ -1,6 +1,11 @@
<div class="border-t border-t-neutral p-2 flex gap-2 items-center max-w-full justify-between">
<div class="flex items-center gap-[8px]">
{{ (this.libraryService.tracks$ | async)?.length ?? 0 }} Songs
<ng-container *ngIf="libraryService.selectedSongs$ | async as selectedSongs">
<ng-container *ngIf="libraryService.tracks$ | async as tracks">
{{ selectedSongs.length ? selectedSongs.length + '/' : '' }}{{ tracks.length }}
</ng-container>
</ng-container>
<button class="btn btn-sm btn-primary" (click)="fileInput.click()">Import Setlist</button>
@if ((this.libraryService.selectedSongs$ | async)!.length > 0) {
<button class="btn btn-sm btn-primary" (click)="exportSelected()">Export Selected Setlist</button>

View File

@@ -8,6 +8,7 @@
</div>
<div
*ngIf="songs.length > 0"
(scroll)="onScroll($event)"
class="basis-2/3 flex-1 overflow-y-auto scrollbar scrollbar-w-2 scrollbar-h-2 scrollbar-track-base-300 scrollbar-thumb-neutral scrollbar-thumb-rounded-full h-[calc(100vh-164px)]">
<table id="resultTable" class="table table-zebra table-pin-rows" [class.table-xs]="settingsService.isCompactTable">
<thead>
@@ -38,12 +39,7 @@
<tbody>
<tr *ngFor="let song of songs; trackBy: trackByFn">
<td>
<input
type="checkbox"
class="checkbox"
#inputBox
(change)="onCheckboxChange(song, $event.target!)"
[checked]="selectedSongs.includes(song)" />
<input type="checkbox" class="checkbox" #inputBox (change)="onCheckboxChange(song, $event.target!)" [checked]="rowIsSelected(song)" />
</td>
<td>{{ song.name }}</td>
<td>{{ song.artist }}</td>
@@ -53,8 +49,16 @@
</tr>
</tbody>
</table>
<div *ngIf="isLoading" class="text-center py-4">Loading...</div>
</div>
<div class="flex align-center justify-center items-center h-[calc(100vh-169.5px)]" *ngIf="songs.length < 1">
<div class="flex align-center justify-center items-center h-[calc(100vh-169.5px)]" *ngIf="songs.length < 1 && !hasSearched">
<p class="text-center" style="font-size: 1.5rem">No songs added!</p>
</div>
<div
class="align-center h-[calc(100vh-169.5px)]"
*ngIf="songs.length < 1 && hasSearched"
style="display: flex; justify-content: center; align-items: center">
<p class="text-center" style="font-size: 1.5rem">No songs found!</p>
</div>

View File

@@ -1,9 +1,11 @@
import { Component, OnInit, OnDestroy } from '@angular/core'
import { SelectionService } from 'src-angular/app/core/services/selection.service'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { SettingsService } from '../../../core/services/settings.service'
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'
import { Subscription } from 'rxjs'
import { LibraryService } from 'src-angular/app/core/services/library.service'
import { SelectionService } from 'src-angular/app/core/services/selection.service'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { SettingsService } from '../../../core/services/settings.service'
type SortColumn = 'name' | 'artist' | 'album' | 'genre' | 'year' | 'charter' | 'length' | 'modifiedTime' | null
@@ -20,6 +22,8 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
allRowsSelected: boolean = false
subscriptions: Subscription[] = []
selectedSongs: ChartData[] = []
isLoading: boolean = false
hasSearched: boolean = false
constructor(
public libraryService: LibraryService,
@@ -56,6 +60,7 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
}
filterSongs(): void {
this.hasSearched = true
this.libraryService.getChartsBySearchTerm(this.searchTerm)
}
@@ -97,6 +102,10 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
}
}
rowIsSelected(song: ChartData): boolean {
return this.selectedSongs.some(selectedSong => selectedSong.md5 === song.md5)
}
trackByFn(index: number): number {
return index
}
@@ -111,6 +120,22 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
}
}
async loadMoreSongs(): Promise<void> {
if (this.isLoading) return
this.isLoading = true
await this.libraryService.loadMoreSongs()
this.isLoading = false
}
@HostListener('scroll', ['$event'])
onScroll(event: Event): void {
const target = event.target as HTMLElement
if (target.scrollHeight - target.scrollTop - target.clientHeight < 100) {
this.loadMoreSongs()
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription.unsubscribe())
}

View File

@@ -1,9 +1,13 @@
import { Injectable, Injector } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { DownloadService } from './download.service'
import { StorageService } from './storage.service'
export const LIBRARY_TABLE_PAGESIZE = 50
@Injectable({
providedIn: 'root',
})
@@ -15,9 +19,11 @@ export class LibraryService {
selectedSongs$ = this._selectedSongs.asObservable()
private _downloadService: DownloadService | null = null
private currentPage = 1
private isLastPage = false
constructor(private injector: Injector, private storageService: StorageService) {
this.storageService.getChartsBySearchTerm().then(library => {
this.storageService.getChartsBySearchTerm(undefined, 1, 50).then(library => {
if (library) {
this._tracks.next(library)
}
@@ -64,12 +70,14 @@ export class LibraryService {
}
addToSelectedSongs(song: ChartData) {
const updatedSelectedSongs = [...this._selectedSongs.value, song]
this._selectedSongs.next(updatedSelectedSongs)
if (!this._selectedSongs.value.some(selectedSong => selectedSong.md5 === song.md5)) {
const updatedSelectedSongs = [...this._selectedSongs.value, song]
this._selectedSongs.next(updatedSelectedSongs)
}
}
removeFromSelectedSongs(song: ChartData) {
const updatedSelectedSongs = this._selectedSongs.value.filter(selectedSong => selectedSong !== song)
const updatedSelectedSongs = this._selectedSongs.value.filter(selectedSong => selectedSong.md5 !== song.md5)
this._selectedSongs.next(updatedSelectedSongs)
}
@@ -95,10 +103,35 @@ export class LibraryService {
this._tracks.next(library)
}
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
const library = await this.storageService.getChartsBySearchTerm(searchTerm)
async loadMoreSongs(): Promise<void> {
if (this.isLastPage)
return
this.currentPage++
const moreSongs = await this.getChartsBySearchTerm()
if (moreSongs.length < LIBRARY_TABLE_PAGESIZE) {
this.isLastPage = true
}
const uniqueSongs = moreSongs.filter(
song => !this._tracks.value.some(track => track.md5 === song.md5)
)
this._tracks.next([...this._tracks.value, ...uniqueSongs])
}
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
if (searchTerm !== undefined) {
this.currentPage = 1
this._tracks.next([])
this.isLastPage = false
}
const library = await this.storageService.getChartsBySearchTerm(searchTerm, this.currentPage, LIBRARY_TABLE_PAGESIZE)
this._tracks.next([...this._tracks.value, ...library])
this._tracks.next(library)
return library
}
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { ChartData, LibrarySearch } from 'src-shared/interfaces/search.interface'
@Injectable({
providedIn: 'root',
@@ -17,8 +18,14 @@ export class StorageService {
return window.electron.invoke.removeCharts(charts)
}
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
return window.electron.invoke.getChartsBySearchTerm(searchTerm)
async getChartsBySearchTerm(searchTerm?: string, page?: number, pageSize?: number): Promise<ChartData[]> {
const librarySearch = {
searchTerm: searchTerm,
page: page,
pageSize: pageSize,
} as LibrarySearch
return window.electron.invoke.getChartsBySearchTerm(librarySearch)
}
async removeAllCharts(): Promise<void> {

View File

@@ -1,7 +1,8 @@
import { ChartData } from 'src-shared/interfaces/search.interface.js'
import { ChartData, LibrarySearch } from 'src-shared/interfaces/search.interface.js'
import { Like } from 'typeorm'
import { dataSource } from './dataSource.js'
import { Chart } from './entities/Chart.js'
import { Like } from 'typeorm'
export class DatabaseService {
async insertChart(chartData: ChartData): Promise<ChartData> {
@@ -12,7 +13,6 @@ export class DatabaseService {
const chartRepository = dataSource.getRepository(Chart)
// if one already exist dont create
const existingChart = await chartRepository.findOneBy({ md5: chartData.md5 })
if (existingChart) {
@@ -61,7 +61,6 @@ export class DatabaseService {
const chartRepository = dataSource.getRepository(Chart)
// delete the array of charts provided using querybulilder
charts.forEach(async chart => {
console.log('removing chart:', chart.name)
await chartRepository.delete({ md5: chart.md5 })
@@ -72,7 +71,7 @@ export class DatabaseService {
}
}
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
async getChartsBySearchTerm(search: LibrarySearch): Promise<ChartData[]> {
try {
if (!dataSource.isInitialized) {
await dataSource.initialize()
@@ -82,22 +81,27 @@ export class DatabaseService {
let charts: Chart[]
if (searchTerm) {
const likeSearchTerm = `%${searchTerm}%`
if (search.searchTerm?.trim()) {
const likeSearchTerm = Like(`%${search.searchTerm}%`)
charts = await chartRepository.find({
where: [
{ name: Like(likeSearchTerm) },
{ album: Like(likeSearchTerm) },
{ artist: Like(likeSearchTerm) },
{ genre: Like(likeSearchTerm) },
{ year: Like(likeSearchTerm) },
{ charter: Like(likeSearchTerm) },
{ name: likeSearchTerm },
{ album: likeSearchTerm },
{ artist: likeSearchTerm },
{ genre: likeSearchTerm },
{ year: likeSearchTerm },
{ charter: likeSearchTerm },
],
skip: (search.page - 1) * search.pageSize,
take: search.pageSize,
})
} else {
charts = await chartRepository.find()
charts = await chartRepository.find({
skip: (search.page - 1) * search.pageSize,
take: search.pageSize,
})
}
return charts as unknown as ChartData[]
} catch (error) {
console.error('Error fetching charts by search term:', error)
@@ -119,7 +123,6 @@ export class DatabaseService {
throw error
}
}
}
export const databaseService = new DatabaseService()

View File

@@ -1,5 +1,6 @@
import { ChartData, LibrarySearch } from 'src-shared/interfaces/search.interface.js'
import { databaseService } from '../database/databaseService.js'
import { ChartData } from 'src-shared/interfaces/search.interface.js'
export async function addChart(chartData: ChartData): Promise<ChartData> {
try {
@@ -37,9 +38,9 @@ export async function removeAllCharts(): Promise<void> {
}
}
export async function getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
export async function getChartsBySearchTerm(search: LibrarySearch): Promise<ChartData[]> {
try {
return await databaseService.getChartsBySearchTerm(searchTerm)
return await databaseService.getChartsBySearchTerm(search)
} catch (error) {
console.error('Error in getChartsHandler:', error)
throw error

View File

@@ -3,9 +3,9 @@ import { UpdateInfo } from 'electron-updater'
import { Settings } from '../Settings.js'
import { Download, DownloadProgress } from './download.interface.js'
import { ChartData, LibrarySearch } from './search.interface.js'
import { ThemeColors } from './theme.interface.js'
import { UpdateProgress } from './update.interface.js'
import { ChartData } from './search.interface.js'
export interface ContextBridgeApi {
invoke: IpcInvokeHandlers
@@ -64,7 +64,7 @@ export interface IpcInvokeEvents {
output: void
}
getChartsBySearchTerm: {
input?: string
input?: LibrarySearch
output: ChartData[]
}
}

View File

@@ -117,6 +117,12 @@ export interface FolderIssue {
description: string
}
export interface LibrarySearch {
searchTerm: string
page: number
pageSize: number
}
export type ChartData = SearchResult['data'][number]
export interface SearchResult {
found: number