diff --git a/src-angular/app/components/library/library-bar/library-bar.component.html b/src-angular/app/components/library/library-bar/library-bar.component.html index 6641094..56d87c3 100644 --- a/src-angular/app/components/library/library-bar/library-bar.component.html +++ b/src-angular/app/components/library/library-bar/library-bar.component.html @@ -1,6 +1,11 @@
- {{ (this.libraryService.tracks$ | async)?.length ?? 0 }} Songs + + + {{ selectedSongs.length ? selectedSongs.length + '/' : '' }}{{ tracks.length }} + + + @if ((this.libraryService.selectedSongs$ | async)!.length > 0) { diff --git a/src-angular/app/components/library/library-table/library-table.component.html b/src-angular/app/components/library/library-table/library-table.component.html index eefd937..996e9f4 100644 --- a/src-angular/app/components/library/library-table/library-table.component.html +++ b/src-angular/app/components/library/library-table/library-table.component.html @@ -8,6 +8,7 @@
@@ -38,12 +39,7 @@ @@ -53,8 +49,16 @@
- + {{ song.name }} {{ song.artist }}
+
Loading...
-
+

No songs added!

+ +
+

No songs found!

+
diff --git a/src-angular/app/components/library/library-table/library-table.component.ts b/src-angular/app/components/library/library-table/library-table.component.ts index 6637ba1..6cde246 100644 --- a/src-angular/app/components/library/library-table/library-table.component.ts +++ b/src-angular/app/components/library/library-table/library-table.component.ts @@ -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 { + 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()) } diff --git a/src-angular/app/core/services/library.service.ts b/src-angular/app/core/services/library.service.ts index ef21466..1aaaeab 100644 --- a/src-angular/app/core/services/library.service.ts +++ b/src-angular/app/core/services/library.service.ts @@ -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 { - const library = await this.storageService.getChartsBySearchTerm(searchTerm) + async loadMoreSongs(): Promise { + 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 { + 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 } } diff --git a/src-angular/app/core/services/storage.service.ts b/src-angular/app/core/services/storage.service.ts index 1800ecd..dce3985 100644 --- a/src-angular/app/core/services/storage.service.ts +++ b/src-angular/app/core/services/storage.service.ts @@ -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 { - return window.electron.invoke.getChartsBySearchTerm(searchTerm) + async getChartsBySearchTerm(searchTerm?: string, page?: number, pageSize?: number): Promise { + const librarySearch = { + searchTerm: searchTerm, + page: page, + pageSize: pageSize, + } as LibrarySearch + + return window.electron.invoke.getChartsBySearchTerm(librarySearch) } async removeAllCharts(): Promise { diff --git a/src-electron/database/databaseService.ts b/src-electron/database/databaseService.ts index 90c0623..d6319b9 100644 --- a/src-electron/database/databaseService.ts +++ b/src-electron/database/databaseService.ts @@ -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 { @@ -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 { + async getChartsBySearchTerm(search: LibrarySearch): Promise { 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() diff --git a/src-electron/ipc/LibraryHandler.ipc.ts b/src-electron/ipc/LibraryHandler.ipc.ts index 72b223f..8268de7 100644 --- a/src-electron/ipc/LibraryHandler.ipc.ts +++ b/src-electron/ipc/LibraryHandler.ipc.ts @@ -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 { try { @@ -37,9 +38,9 @@ export async function removeAllCharts(): Promise { } } -export async function getChartsBySearchTerm(searchTerm?: string): Promise { +export async function getChartsBySearchTerm(search: LibrarySearch): Promise { try { - return await databaseService.getChartsBySearchTerm(searchTerm) + return await databaseService.getChartsBySearchTerm(search) } catch (error) { console.error('Error in getChartsHandler:', error) throw error diff --git a/src-shared/interfaces/ipc.interface.ts b/src-shared/interfaces/ipc.interface.ts index 317e871..c611546 100644 --- a/src-shared/interfaces/ipc.interface.ts +++ b/src-shared/interfaces/ipc.interface.ts @@ -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[] } } diff --git a/src-shared/interfaces/search.interface.ts b/src-shared/interfaces/search.interface.ts index f35e1b7..18a4ead 100644 --- a/src-shared/interfaces/search.interface.ts +++ b/src-shared/interfaces/search.interface.ts @@ -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