mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-23 10:55:07 +00:00
[Library] Add Infinite Scrolling
Add infinite scrolling and fix selecting rows
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user