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="border-t border-t-neutral p-2 flex gap-2 items-center max-w-full justify-between">
|
||||||
<div class="flex items-center gap-[8px]">
|
<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>
|
<button class="btn btn-sm btn-primary" (click)="fileInput.click()">Import Setlist</button>
|
||||||
@if ((this.libraryService.selectedSongs$ | async)!.length > 0) {
|
@if ((this.libraryService.selectedSongs$ | async)!.length > 0) {
|
||||||
<button class="btn btn-sm btn-primary" (click)="exportSelected()">Export Selected Setlist</button>
|
<button class="btn btn-sm btn-primary" (click)="exportSelected()">Export Selected Setlist</button>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="songs.length > 0"
|
*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)]">
|
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">
|
<table id="resultTable" class="table table-zebra table-pin-rows" [class.table-xs]="settingsService.isCompactTable">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -38,12 +39,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let song of songs; trackBy: trackByFn">
|
<tr *ngFor="let song of songs; trackBy: trackByFn">
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input type="checkbox" class="checkbox" #inputBox (change)="onCheckboxChange(song, $event.target!)" [checked]="rowIsSelected(song)" />
|
||||||
type="checkbox"
|
|
||||||
class="checkbox"
|
|
||||||
#inputBox
|
|
||||||
(change)="onCheckboxChange(song, $event.target!)"
|
|
||||||
[checked]="selectedSongs.includes(song)" />
|
|
||||||
</td>
|
</td>
|
||||||
<td>{{ song.name }}</td>
|
<td>{{ song.name }}</td>
|
||||||
<td>{{ song.artist }}</td>
|
<td>{{ song.artist }}</td>
|
||||||
@@ -53,8 +49,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div *ngIf="isLoading" class="text-center py-4">Loading...</div>
|
||||||
</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>
|
<p class="text-center" style="font-size: 1.5rem">No songs added!</p>
|
||||||
</div>
|
</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 { Component, HostListener, OnDestroy, OnInit } 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 { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { LibraryService } from 'src-angular/app/core/services/library.service'
|
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
|
type SortColumn = 'name' | 'artist' | 'album' | 'genre' | 'year' | 'charter' | 'length' | 'modifiedTime' | null
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
|
|||||||
allRowsSelected: boolean = false
|
allRowsSelected: boolean = false
|
||||||
subscriptions: Subscription[] = []
|
subscriptions: Subscription[] = []
|
||||||
selectedSongs: ChartData[] = []
|
selectedSongs: ChartData[] = []
|
||||||
|
isLoading: boolean = false
|
||||||
|
hasSearched: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public libraryService: LibraryService,
|
public libraryService: LibraryService,
|
||||||
@@ -56,6 +60,7 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterSongs(): void {
|
filterSongs(): void {
|
||||||
|
this.hasSearched = true
|
||||||
this.libraryService.getChartsBySearchTerm(this.searchTerm)
|
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 {
|
trackByFn(index: number): number {
|
||||||
return index
|
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 {
|
ngOnDestroy(): void {
|
||||||
this.subscriptions.forEach(subscription => subscription.unsubscribe())
|
this.subscriptions.forEach(subscription => subscription.unsubscribe())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Injectable, Injector } from '@angular/core'
|
import { Injectable, Injector } from '@angular/core'
|
||||||
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||||
|
|
||||||
import { DownloadService } from './download.service'
|
import { DownloadService } from './download.service'
|
||||||
import { StorageService } from './storage.service'
|
import { StorageService } from './storage.service'
|
||||||
|
|
||||||
|
export const LIBRARY_TABLE_PAGESIZE = 50
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -15,9 +19,11 @@ export class LibraryService {
|
|||||||
selectedSongs$ = this._selectedSongs.asObservable()
|
selectedSongs$ = this._selectedSongs.asObservable()
|
||||||
|
|
||||||
private _downloadService: DownloadService | null = null
|
private _downloadService: DownloadService | null = null
|
||||||
|
private currentPage = 1
|
||||||
|
private isLastPage = false
|
||||||
|
|
||||||
constructor(private injector: Injector, private storageService: StorageService) {
|
constructor(private injector: Injector, private storageService: StorageService) {
|
||||||
this.storageService.getChartsBySearchTerm().then(library => {
|
this.storageService.getChartsBySearchTerm(undefined, 1, 50).then(library => {
|
||||||
if (library) {
|
if (library) {
|
||||||
this._tracks.next(library)
|
this._tracks.next(library)
|
||||||
}
|
}
|
||||||
@@ -64,12 +70,14 @@ export class LibraryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addToSelectedSongs(song: ChartData) {
|
addToSelectedSongs(song: ChartData) {
|
||||||
const updatedSelectedSongs = [...this._selectedSongs.value, song]
|
if (!this._selectedSongs.value.some(selectedSong => selectedSong.md5 === song.md5)) {
|
||||||
this._selectedSongs.next(updatedSelectedSongs)
|
const updatedSelectedSongs = [...this._selectedSongs.value, song]
|
||||||
|
this._selectedSongs.next(updatedSelectedSongs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromSelectedSongs(song: ChartData) {
|
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)
|
this._selectedSongs.next(updatedSelectedSongs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +103,35 @@ export class LibraryService {
|
|||||||
this._tracks.next(library)
|
this._tracks.next(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
|
async loadMoreSongs(): Promise<void> {
|
||||||
const library = await this.storageService.getChartsBySearchTerm(searchTerm)
|
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
|
return library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
|
||||||
|
import { ChartData, LibrarySearch } from 'src-shared/interfaces/search.interface'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -17,8 +18,14 @@ export class StorageService {
|
|||||||
return window.electron.invoke.removeCharts(charts)
|
return window.electron.invoke.removeCharts(charts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChartsBySearchTerm(searchTerm?: string): Promise<ChartData[]> {
|
async getChartsBySearchTerm(searchTerm?: string, page?: number, pageSize?: number): Promise<ChartData[]> {
|
||||||
return window.electron.invoke.getChartsBySearchTerm(searchTerm)
|
const librarySearch = {
|
||||||
|
searchTerm: searchTerm,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
} as LibrarySearch
|
||||||
|
|
||||||
|
return window.electron.invoke.getChartsBySearchTerm(librarySearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAllCharts(): Promise<void> {
|
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 { dataSource } from './dataSource.js'
|
||||||
import { Chart } from './entities/Chart.js'
|
import { Chart } from './entities/Chart.js'
|
||||||
import { Like } from 'typeorm'
|
|
||||||
|
|
||||||
export class DatabaseService {
|
export class DatabaseService {
|
||||||
async insertChart(chartData: ChartData): Promise<ChartData> {
|
async insertChart(chartData: ChartData): Promise<ChartData> {
|
||||||
@@ -12,7 +13,6 @@ export class DatabaseService {
|
|||||||
|
|
||||||
const chartRepository = dataSource.getRepository(Chart)
|
const chartRepository = dataSource.getRepository(Chart)
|
||||||
|
|
||||||
// if one already exist dont create
|
|
||||||
const existingChart = await chartRepository.findOneBy({ md5: chartData.md5 })
|
const existingChart = await chartRepository.findOneBy({ md5: chartData.md5 })
|
||||||
|
|
||||||
if (existingChart) {
|
if (existingChart) {
|
||||||
@@ -61,7 +61,6 @@ export class DatabaseService {
|
|||||||
|
|
||||||
const chartRepository = dataSource.getRepository(Chart)
|
const chartRepository = dataSource.getRepository(Chart)
|
||||||
|
|
||||||
// delete the array of charts provided using querybulilder
|
|
||||||
charts.forEach(async chart => {
|
charts.forEach(async chart => {
|
||||||
console.log('removing chart:', chart.name)
|
console.log('removing chart:', chart.name)
|
||||||
await chartRepository.delete({ md5: chart.md5 })
|
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 {
|
try {
|
||||||
if (!dataSource.isInitialized) {
|
if (!dataSource.isInitialized) {
|
||||||
await dataSource.initialize()
|
await dataSource.initialize()
|
||||||
@@ -82,22 +81,27 @@ export class DatabaseService {
|
|||||||
|
|
||||||
let charts: Chart[]
|
let charts: Chart[]
|
||||||
|
|
||||||
if (searchTerm) {
|
if (search.searchTerm?.trim()) {
|
||||||
const likeSearchTerm = `%${searchTerm}%`
|
const likeSearchTerm = Like(`%${search.searchTerm}%`)
|
||||||
|
|
||||||
charts = await chartRepository.find({
|
charts = await chartRepository.find({
|
||||||
where: [
|
where: [
|
||||||
{ name: Like(likeSearchTerm) },
|
{ name: likeSearchTerm },
|
||||||
{ album: Like(likeSearchTerm) },
|
{ album: likeSearchTerm },
|
||||||
{ artist: Like(likeSearchTerm) },
|
{ artist: likeSearchTerm },
|
||||||
{ genre: Like(likeSearchTerm) },
|
{ genre: likeSearchTerm },
|
||||||
{ year: Like(likeSearchTerm) },
|
{ year: likeSearchTerm },
|
||||||
{ charter: Like(likeSearchTerm) },
|
{ charter: likeSearchTerm },
|
||||||
],
|
],
|
||||||
|
skip: (search.page - 1) * search.pageSize,
|
||||||
|
take: search.pageSize,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
charts = await chartRepository.find()
|
charts = await chartRepository.find({
|
||||||
|
skip: (search.page - 1) * search.pageSize,
|
||||||
|
take: search.pageSize,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return charts as unknown as ChartData[]
|
return charts as unknown as ChartData[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching charts by search term:', error)
|
console.error('Error fetching charts by search term:', error)
|
||||||
@@ -119,7 +123,6 @@ export class DatabaseService {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const databaseService = new DatabaseService()
|
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 { databaseService } from '../database/databaseService.js'
|
||||||
import { ChartData } from 'src-shared/interfaces/search.interface.js'
|
|
||||||
|
|
||||||
export async function addChart(chartData: ChartData): Promise<ChartData> {
|
export async function addChart(chartData: ChartData): Promise<ChartData> {
|
||||||
try {
|
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 {
|
try {
|
||||||
return await databaseService.getChartsBySearchTerm(searchTerm)
|
return await databaseService.getChartsBySearchTerm(search)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getChartsHandler:', error)
|
console.error('Error in getChartsHandler:', error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { UpdateInfo } from 'electron-updater'
|
|||||||
|
|
||||||
import { Settings } from '../Settings.js'
|
import { Settings } from '../Settings.js'
|
||||||
import { Download, DownloadProgress } from './download.interface.js'
|
import { Download, DownloadProgress } from './download.interface.js'
|
||||||
|
import { ChartData, LibrarySearch } from './search.interface.js'
|
||||||
import { ThemeColors } from './theme.interface.js'
|
import { ThemeColors } from './theme.interface.js'
|
||||||
import { UpdateProgress } from './update.interface.js'
|
import { UpdateProgress } from './update.interface.js'
|
||||||
import { ChartData } from './search.interface.js'
|
|
||||||
|
|
||||||
export interface ContextBridgeApi {
|
export interface ContextBridgeApi {
|
||||||
invoke: IpcInvokeHandlers
|
invoke: IpcInvokeHandlers
|
||||||
@@ -64,7 +64,7 @@ export interface IpcInvokeEvents {
|
|||||||
output: void
|
output: void
|
||||||
}
|
}
|
||||||
getChartsBySearchTerm: {
|
getChartsBySearchTerm: {
|
||||||
input?: string
|
input?: LibrarySearch
|
||||||
output: ChartData[]
|
output: ChartData[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ export interface FolderIssue {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LibrarySearch {
|
||||||
|
searchTerm: string
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ChartData = SearchResult['data'][number]
|
export type ChartData = SearchResult['data'][number]
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
found: number
|
found: number
|
||||||
|
|||||||
Reference in New Issue
Block a user