- 1 && filteredSongs.length < 1"
- style="display: flex; justify-content: center; align-items: center">
- 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 c72ae06..6637ba1 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
@@ -16,7 +16,6 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
songs: ChartData[] = []
sortDirection: 'asc' | 'desc' = 'asc'
sortColumn: SortColumn = null
- filteredSongs: ChartData[] = []
searchTerm: string = ''
allRowsSelected: boolean = false
subscriptions: Subscription[] = []
@@ -45,10 +44,9 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
this.libraryService.tracks$
.subscribe(tracks => {
this.songs = tracks
- this.filterSongs()
})
)
- this.filteredSongs = [...this.songs]
+
this.subscriptions.push(
this.libraryService.selectedSongs$
.subscribe(songs =>
@@ -58,20 +56,11 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
}
filterSongs(): void {
- const term = this.searchTerm.toLowerCase()
- this.filteredSongs = this.songs.filter(
- song =>
- song.name?.toLowerCase().includes(term) ||
- song.artist?.toLowerCase().includes(term) ||
- song.album?.toLowerCase().includes(term) ||
- song.genre?.toLowerCase().includes(term) ||
- song.year?.toLowerCase().includes(term) ||
- song.charter?.toLowerCase().includes(term)
- )
+ this.libraryService.getChartsBySearchTerm(this.searchTerm)
}
onColClicked(column: SortColumn) {
- if (this.filteredSongs.length === 0) { return }
+ if (this.songs.length === 0) { return }
if (this.sortColumn !== column) {
this.sortColumn = column
@@ -84,7 +73,7 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
}
if (this.sortColumn) {
- this.filteredSongs.sort((a, b) => {
+ this.songs.sort((a, b) => {
const valueA = a[this.sortColumn! as keyof ChartData]
const valueB = b[this.sortColumn! as keyof ChartData]
@@ -116,7 +105,7 @@ export class LibraryTableComponent implements OnInit, OnDestroy {
this.allRowsSelected = !this.allRowsSelected
if (this.allRowsSelected) {
- this.filteredSongs.forEach(song => this.libraryService.addToSelectedSongs(song))
+ this.songs.forEach(song => this.libraryService.addToSelectedSongs(song))
} else {
this.libraryService.clearSelectedSongs()
}
diff --git a/src-angular/app/core/services/library.service.ts b/src-angular/app/core/services/library.service.ts
index 95b8f84..ef21466 100644
--- a/src-angular/app/core/services/library.service.ts
+++ b/src-angular/app/core/services/library.service.ts
@@ -2,8 +2,7 @@ import { Injectable, Injector } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { ChartData } from 'src-shared/interfaces/search.interface'
import { DownloadService } from './download.service'
-
-const LibraryStorageIdentifyer: string = "library"
+import { StorageService } from './storage.service'
@Injectable({
providedIn: 'root',
@@ -17,42 +16,39 @@ export class LibraryService {
private _downloadService: DownloadService | null = null
- constructor(private injector: Injector) {
- const library = localStorage.getItem(LibraryStorageIdentifyer)
-
- if (library) {
- this._tracks.next(JSON.parse(library))
- }
+ constructor(private injector: Injector, private storageService: StorageService) {
+ this.storageService.getChartsBySearchTerm().then(library => {
+ if (library) {
+ this._tracks.next(library)
+ }
+ })
}
private get downloadService(): DownloadService {
if (!this._downloadService) {
this._downloadService = this.injector.get(DownloadService)
}
- return this._downloadService
- }
- getPlaylist() {
- return this._tracks.value
+ return this._downloadService
}
libraryAdd(chart: ChartData) {
const updatedTracks = [...this._tracks.value, chart]
this._tracks.next(updatedTracks)
- localStorage.setItem(LibraryStorageIdentifyer, JSON.stringify(updatedTracks))
+
+ this.storageService.addChart(chart)
}
downloadLibrary(songs: ChartData[]) {
songs.forEach(track => {
- if (!this._tracks.value.includes(track)) {
- this.downloadService.addDownload(track)
- }
+ this.downloadService.addDownload(track)
})
}
storeLibrary() {
const fakeLink = document.createElement('a')
const file = new Blob([JSON.stringify(this._tracks.value)], { type: 'application/json' })
+
fakeLink.href = URL.createObjectURL(file)
fakeLink.download = 'songs.library'
fakeLink.click()
@@ -61,6 +57,7 @@ export class LibraryService {
storeSelectedSongs() {
const fakeLink = document.createElement('a')
const file = new Blob([JSON.stringify(this._selectedSongs.value)], { type: 'application/json' })
+
fakeLink.href = URL.createObjectURL(file)
fakeLink.download = 'selected.library'
fakeLink.click()
@@ -80,16 +77,28 @@ export class LibraryService {
this._selectedSongs.next([])
}
- removeFromPlaylist() {
- this._selectedSongs.value.forEach(selectedSong => {
- const updatedTracks = this._tracks.value.filter(track => track !== selectedSong)
+ removeFromLibrary() {
+ this._selectedSongs.value.forEach((selectedSong: ChartData) => {
+ const updatedTracks = this._tracks.value.filter(track => track !== selectedSong) as ChartData[]
this._tracks.next(updatedTracks)
- localStorage.setItem(LibraryStorageIdentifyer, JSON.stringify(updatedTracks))
+ this.storageService.removeChart(selectedSong.md5)
})
+
+ this.clearSelectedSongs()
}
- clearPlaylist() {
- this._tracks.next([])
- localStorage.removeItem(LibraryStorageIdentifyer)
+ async clearLibrary() {
+ this.storageService.removeAllCharts()
+ this.clearSelectedSongs()
+
+ const library = await this.storageService.getChartsBySearchTerm()
+ this._tracks.next(library)
+ }
+
+ async getChartsBySearchTerm(searchTerm?: string): Promise {
+ const library = await this.storageService.getChartsBySearchTerm(searchTerm)
+
+ 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
new file mode 100644
index 0000000..1800ecd
--- /dev/null
+++ b/src-angular/app/core/services/storage.service.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core'
+import { ChartData } from 'src-shared/interfaces/search.interface'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StorageService {
+ async addChart(chartData: ChartData): Promise {
+ return window.electron.invoke.addChart(chartData)
+ }
+
+ async removeChart(md5: string): Promise {
+ return window.electron.invoke.removeChart(md5)
+ }
+
+ async removeCharts(charts: ChartData[]): Promise {
+ return window.electron.invoke.removeCharts(charts)
+ }
+
+ async getChartsBySearchTerm(searchTerm?: string): Promise {
+ return window.electron.invoke.getChartsBySearchTerm(searchTerm)
+ }
+
+ async removeAllCharts(): Promise {
+ return window.electron.emit.removeAllCharts()
+ }
+}
diff --git a/src-electron/IpcHandler.ts b/src-electron/IpcHandler.ts
index 6222482..064e579 100644
--- a/src-electron/IpcHandler.ts
+++ b/src-electron/IpcHandler.ts
@@ -1,6 +1,7 @@
import { IpcInvokeHandlers, IpcToMainEmitHandlers } from '../src-shared/interfaces/ipc.interface.js'
import { download } from './ipc/DownloadHandler.ipc.js'
import { scanIssues } from './ipc/issue-scan/IssueScanHandler.ipc.js'
+import { addChart, getChartsBySearchTerm, removeAllCharts, removeChart, removeCharts } from './ipc/LibraryHandler.ipc.js'
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc.js'
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.ipc.js'
import { getPlatform, getThemeColors, isMaximized, maximize, minimize, openUrl, quit, restore, showFile, showFolder, showOpenDialog, toggleDevTools } from './ipc/UtilHandlers.ipc.js'
@@ -14,6 +15,10 @@ export function getIpcInvokeHandlers(): IpcInvokeHandlers {
isMaximized,
showOpenDialog,
getThemeColors,
+ addChart,
+ removeChart,
+ removeCharts,
+ getChartsBySearchTerm,
}
}
@@ -33,5 +38,6 @@ export function getIpcToMainEmitHandlers(): IpcToMainEmitHandlers {
showFile,
showFolder,
scanIssues,
+ removeAllCharts,
}
}
diff --git a/src-electron/database/dataSource.ts b/src-electron/database/dataSource.ts
new file mode 100644
index 0000000..d0f116f
--- /dev/null
+++ b/src-electron/database/dataSource.ts
@@ -0,0 +1,18 @@
+import { DataSource } from 'typeorm'
+import { Chart } from './entities/Chart.js'
+import { Init1743124434920 } from './migrations/1743124434920-init.js'
+
+const migrations = [Init1743124434920]
+const entities = [Chart]
+
+export const dataSource = new DataSource({
+ type: "sqlite",
+ database: "library.sqlite",
+ entities: entities,
+ // Configure migrations to use a folder that contains your migration files:
+ migrations: migrations,
+ // Keep synchronize off when using migrations in production
+ synchronize: false,
+ logging: true,
+ migrationsRun: true,
+})
diff --git a/src-electron/database/databaseService.ts b/src-electron/database/databaseService.ts
new file mode 100644
index 0000000..90c0623
--- /dev/null
+++ b/src-electron/database/databaseService.ts
@@ -0,0 +1,125 @@
+import { ChartData } from 'src-shared/interfaces/search.interface.js'
+import { dataSource } from './dataSource.js'
+import { Chart } from './entities/Chart.js'
+import { Like } from 'typeorm'
+
+export class DatabaseService {
+ async insertChart(chartData: ChartData): Promise {
+ try {
+ if (!dataSource.isInitialized) {
+ await dataSource.initialize()
+ }
+
+ const chartRepository = dataSource.getRepository(Chart)
+
+ // if one already exist dont create
+ const existingChart = await chartRepository.findOneBy({ md5: chartData.md5 })
+
+ if (existingChart) {
+ return existingChart as unknown as ChartData
+ }
+
+ const newChart = chartRepository.create({
+ name: chartData.name!,
+ album: chartData.album!,
+ artist: chartData.artist!,
+ genre: chartData.genre!,
+ year: chartData.year!,
+ charter: chartData.charter!,
+ md5: chartData.md5,
+ hasVideoBackground: chartData.hasVideoBackground,
+ })
+
+ return await chartRepository.save(newChart) as unknown as ChartData
+ } catch (error) {
+ console.error('Error inserting chart:', error)
+ throw error
+ }
+ }
+
+ async removeChart(md5: string): Promise {
+ try {
+ if (!dataSource.isInitialized) {
+ await dataSource.initialize()
+ }
+
+ const chartRepository = dataSource.getRepository(Chart)
+
+ await chartRepository.delete({ md5 })
+
+ } catch (error) {
+ console.error('Error removing chart:', error)
+ throw error
+ }
+ }
+
+ async removeCharts(charts: ChartData[]): Promise {
+ try {
+ if (!dataSource.isInitialized) {
+ await dataSource.initialize()
+ }
+
+ 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 })
+ })
+ } catch (error) {
+ console.error('Error removing charts:', error)
+ throw error
+ }
+ }
+
+ async getChartsBySearchTerm(searchTerm?: string): Promise {
+ try {
+ if (!dataSource.isInitialized) {
+ await dataSource.initialize()
+ }
+
+ const chartRepository = dataSource.getRepository(Chart)
+
+ let charts: Chart[]
+
+ if (searchTerm) {
+ const likeSearchTerm = `%${searchTerm}%`
+ charts = await chartRepository.find({
+ where: [
+ { name: Like(likeSearchTerm) },
+ { album: Like(likeSearchTerm) },
+ { artist: Like(likeSearchTerm) },
+ { genre: Like(likeSearchTerm) },
+ { year: Like(likeSearchTerm) },
+ { charter: Like(likeSearchTerm) },
+ ],
+ })
+ } else {
+ charts = await chartRepository.find()
+ }
+
+ return charts as unknown as ChartData[]
+ } catch (error) {
+ console.error('Error fetching charts by search term:', error)
+ throw error
+ }
+ }
+
+ async removeAllCharts(): Promise {
+ try {
+ if (!dataSource.isInitialized) {
+ await dataSource.initialize()
+ }
+
+ const chartRepository = dataSource.getRepository(Chart)
+
+ await chartRepository.clear()
+ } catch (error) {
+ console.error('Error removing all charts:', error)
+ throw error
+ }
+ }
+
+}
+
+export const databaseService = new DatabaseService()
diff --git a/src-electron/database/entities/Chart.ts b/src-electron/database/entities/Chart.ts
new file mode 100644
index 0000000..64ff7b0
--- /dev/null
+++ b/src-electron/database/entities/Chart.ts
@@ -0,0 +1,31 @@
+import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Chart {
+ @PrimaryGeneratedColumn('uuid')
+ id: string
+
+ @Column()
+ md5: string
+
+ @Column()
+ hasVideoBackground: boolean
+
+ @Column()
+ charter: string
+
+ @Column()
+ name: string
+
+ @Column()
+ artist: string
+
+ @Column()
+ album: string
+
+ @Column()
+ genre: string
+
+ @Column()
+ year: string
+}
diff --git a/src-electron/database/migrations/1743124434920-init.ts b/src-electron/database/migrations/1743124434920-init.ts
new file mode 100644
index 0000000..dc9ef09
--- /dev/null
+++ b/src-electron/database/migrations/1743124434920-init.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Init1743124434920 implements MigrationInterface {
+ name = 'Init1743124434920'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE "chart" ("id" varchar PRIMARY KEY NOT NULL, "md5" varchar NOT NULL, "hasVideoBackground" boolean NOT NULL, "charter" varchar NOT NULL, "name" varchar NOT NULL, "artist" varchar NOT NULL, "album" varchar NOT NULL, "genre" varchar NOT NULL, "year" varchar NOT NULL)`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP TABLE "chart"`);
+ }
+
+}
diff --git a/src-electron/database/readme.md b/src-electron/database/readme.md
new file mode 100644
index 0000000..99c52f6
--- /dev/null
+++ b/src-electron/database/readme.md
@@ -0,0 +1,10 @@
+## Migrations
+In order to create a new migration, there is a some steps to go through.
+
+1. Run ``npm run migration:add --name `` This currently work for Windows machines. If using Linux or Mac run this instead ``npx typeorm-ts-node-esm migration:generate ./src-electron/database/migrations/ -d ./src-electron/database/dataSource.ts``
+
+2. Go to ``./src-electron/database/dataSource.ts`` and add the newly
+generated migration to the migrations array and entity variables. In that way it will automatically apply the latest changes to the database on startup.
+
+## The database
+A Sqlite database file is automatically created on startup named Library, it will be placed in the same directory as the executable.
diff --git a/src-electron/ipc/LibraryHandler.ipc.ts b/src-electron/ipc/LibraryHandler.ipc.ts
new file mode 100644
index 0000000..72b223f
--- /dev/null
+++ b/src-electron/ipc/LibraryHandler.ipc.ts
@@ -0,0 +1,47 @@
+import { databaseService } from '../database/databaseService.js'
+import { ChartData } from 'src-shared/interfaces/search.interface.js'
+
+export async function addChart(chartData: ChartData): Promise {
+ try {
+ return await databaseService.insertChart(chartData)
+ } catch (error) {
+ console.error('Error in addChartHandler:', error)
+ throw error
+ }
+}
+
+export async function removeChart(md5: string): Promise {
+ try {
+ await databaseService.removeChart(md5)
+ } catch (error) {
+ console.error('Error in removeChartHandler:', error)
+ throw error
+ }
+}
+
+export async function removeCharts(charts: ChartData[]): Promise {
+ try {
+ await databaseService.removeCharts(charts)
+ } catch (error) {
+ console.error('Error in removeChartsHandler:', error)
+ throw error
+ }
+}
+
+export async function removeAllCharts(): Promise {
+ try {
+ await databaseService.removeAllCharts()
+ } catch (error) {
+ console.error('Error in removeAllChartsHandler:', error)
+ throw error
+ }
+}
+
+export async function getChartsBySearchTerm(searchTerm?: string): Promise {
+ try {
+ return await databaseService.getChartsBySearchTerm(searchTerm)
+ } catch (error) {
+ console.error('Error in getChartsHandler:', error)
+ throw error
+ }
+}
diff --git a/src-electron/main.ts b/src-electron/main.ts
index 23c8e0e..0dfb1c3 100644
--- a/src-electron/main.ts
+++ b/src-electron/main.ts
@@ -3,12 +3,13 @@ import electronUnhandled from 'electron-unhandled'
import windowStateKeeper from 'electron-window-state'
import * as path from 'path'
import * as url from 'url'
-
+import "reflect-metadata"
import { IpcFromMainEmitEvents } from '../src-shared/interfaces/ipc.interface.js'
import { dataPath } from '../src-shared/Paths.js'
import { settings } from './ipc/SettingsHandler.ipc.js'
import { retryUpdate } from './ipc/UpdateHandler.ipc.js'
import { getIpcInvokeHandlers, getIpcToMainEmitHandlers } from './IpcHandler.js'
+import { dataSource } from './database/dataSource.js'
electronUnhandled({ showDialog: true, logger: err => console.log('Error: Unhandled Rejection:', err) })
@@ -26,6 +27,14 @@ app.on('ready', async () => {
if (!isDevBuild) {
retryUpdate()
}
+
+ // Initialize the database
+ dataSource.initialize().then(() => {
+ console.log('Database initialized')
+ }
+ ).catch(error => {
+ console.error('Error initializing database:', error)
+ })
})
/**
diff --git a/src-electron/preload.ts b/src-electron/preload.ts
index ee96da8..4064e59 100644
--- a/src-electron/preload.ts
+++ b/src-electron/preload.ts
@@ -25,6 +25,10 @@ const electronApi: ContextBridgeApi = {
isMaximized: getInvoker('isMaximized'),
showOpenDialog: getInvoker('showOpenDialog'),
getThemeColors: getInvoker('getThemeColors'),
+ addChart: getInvoker('addChart'),
+ removeChart: getInvoker('removeChart'),
+ removeCharts: getInvoker('removeCharts'),
+ getChartsBySearchTerm: getInvoker('getChartsBySearchTerm'),
},
emit: {
download: getEmitter('download'),
@@ -41,6 +45,7 @@ const electronApi: ContextBridgeApi = {
showFolder: getEmitter('showFolder'),
showFile: getEmitter('showFile'),
scanIssues: getEmitter('scanIssues'),
+ removeAllCharts: getEmitter('removeAllCharts'),
},
on: {
errorLog: getListenerAdder('errorLog'),
diff --git a/src-shared/interfaces/ipc.interface.ts b/src-shared/interfaces/ipc.interface.ts
index 7996797..317e871 100644
--- a/src-shared/interfaces/ipc.interface.ts
+++ b/src-shared/interfaces/ipc.interface.ts
@@ -5,6 +5,7 @@ import { Settings } from '../Settings.js'
import { Download, DownloadProgress } from './download.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
@@ -50,6 +51,22 @@ export interface IpcInvokeEvents {
input: string
output: ThemeColors | null
}
+ addChart: {
+ input: ChartData
+ output: ChartData
+ }
+ removeChart: {
+ input: string
+ output: void
+ }
+ removeCharts: {
+ input: ChartData[]
+ output: void
+ }
+ getChartsBySearchTerm: {
+ input?: string
+ output: ChartData[]
+ }
}
export type IpcInvokeHandlers = {
@@ -75,6 +92,7 @@ export interface IpcToMainEmitEvents {
showFolder: string
showFile: string
scanIssues: void
+ removeAllCharts: void
}
export type IpcToMainEmitHandlers = {
|