mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-09 05:09:39 +00:00
Initial Browse UI and initial song search
This commit is contained in:
103
package-lock.json
generated
103
package-lock.json
generated
@@ -2252,6 +2252,11 @@
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/cli-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cli-color/-/cli-color-2.0.0.tgz",
|
||||
"integrity": "sha512-E2Oisr73FjwxMHkYU6RcN9P9mmrbG4TNQMIebWhazYxOgWRzA7s4hM+DtAs6ZwiwKFbPst42v1XUAC1APIhRJA=="
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
@@ -2287,15 +2292,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/jquery": {
|
||||
"version": "3.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
|
||||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
||||
@@ -2308,17 +2304,19 @@
|
||||
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mysql": {
|
||||
"version": "2.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.8.tgz",
|
||||
"integrity": "sha512-l0TUdg6KDEaLO75/yjdjksobJDRWv8iZlpRfv/WW1lQZCQDKdTDnKCkeH10oapzP/JTuKiTy6Cvq/sm/0GgcUw==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "8.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz",
|
||||
"integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ=="
|
||||
},
|
||||
"@types/sizzle": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
|
||||
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/source-list-map": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
|
||||
@@ -3446,6 +3444,11 @@
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
||||
@@ -4205,6 +4208,19 @@
|
||||
"integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==",
|
||||
"dev": true
|
||||
},
|
||||
"cli-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz",
|
||||
"integrity": "sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==",
|
||||
"requires": {
|
||||
"ansi-regex": "^2.1.1",
|
||||
"d": "^1.0.1",
|
||||
"es5-ext": "^0.10.51",
|
||||
"es6-iterator": "^2.0.3",
|
||||
"memoizee": "^0.4.14",
|
||||
"timers-ext": "^0.1.7"
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -6109,6 +6125,15 @@
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
||||
"dev": true
|
||||
},
|
||||
"event-emitter": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
|
||||
@@ -10428,6 +10453,14 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"lru-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
|
||||
"integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=",
|
||||
"requires": {
|
||||
"es5-ext": "~0.10.2"
|
||||
}
|
||||
},
|
||||
"macos-release": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
|
||||
@@ -10612,6 +10645,21 @@
|
||||
"p-is-promise": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"memoizee": {
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
|
||||
"integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.45",
|
||||
"es6-weak-map": "^2.0.2",
|
||||
"event-emitter": "^0.3.5",
|
||||
"is-promise": "^2.1",
|
||||
"lru-queue": "0.1",
|
||||
"next-tick": "1",
|
||||
"timers-ext": "^0.1.5"
|
||||
}
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
@@ -10971,6 +11019,17 @@
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||
},
|
||||
"mysql": {
|
||||
"version": "2.18.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
|
||||
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
|
||||
"requires": {
|
||||
"bignumber.js": "9.0.0",
|
||||
"readable-stream": "2.3.7",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sqlstring": "2.3.1"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
||||
@@ -13803,6 +13862,11 @@
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sqlstring": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
|
||||
"integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
|
||||
@@ -14367,6 +14431,15 @@
|
||||
"setimmediate": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"timers-ext": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
|
||||
"integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
|
||||
"requires": {
|
||||
"es5-ext": "~0.10.46",
|
||||
"next-tick": "1"
|
||||
}
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
"@angular/platform-browser": "~8.2.14",
|
||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||
"@angular/router": "~8.2.14",
|
||||
"@types/cli-color": "^2.0.0",
|
||||
"@types/mysql": "^2.15.8",
|
||||
"cli-color": "^2.0.0",
|
||||
"fomantic-ui": "^2.8.3",
|
||||
"jquery": "^3.4.1",
|
||||
"mysql": "^2.18.1",
|
||||
"rxjs": "~6.4.0",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.9.1"
|
||||
@@ -45,7 +49,6 @@
|
||||
"@angular/cli": "~8.3.24",
|
||||
"@angular/compiler-cli": "~8.2.14",
|
||||
"@angular/language-service": "~8.2.14",
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/node": "~8.9.4",
|
||||
"@typescript-eslint/eslint-plugin": "^2.19.0",
|
||||
"@typescript-eslint/parser": "^2.19.0",
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
<app-toolbar></app-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||
<app-toolbar></app-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -4,13 +4,21 @@ import { NgModule } from '@angular/core'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { ToolbarComponent } from './components/toolbar/toolbar.component'
|
||||
import { BrowseComponent } from './components/browse/browse.component'
|
||||
import { BrowseComponent } from './components/browse/browse.component';
|
||||
import { SearchBarComponent } from './components/browse/search-bar/search-bar.component';
|
||||
import { StatusBarComponent } from './components/browse/status-bar/status-bar.component';
|
||||
import { ResultTableComponent } from './components/browse/result-table/result-table.component';
|
||||
import { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ToolbarComponent,
|
||||
BrowseComponent
|
||||
BrowseComponent,
|
||||
SearchBarComponent,
|
||||
StatusBarComponent,
|
||||
ResultTableComponent,
|
||||
ChartSidebarComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
<p>browse works!</p>
|
||||
<app-search-bar (resultsUpdated)="resultTable.results = $event"></app-search-bar>
|
||||
<div class="ui celled two column grid">
|
||||
<div class="row">
|
||||
<div class="column twelve wide">
|
||||
<app-result-table #resultTable></app-result-table>
|
||||
</div>
|
||||
<div id="sidebar-column" class="column four wide">
|
||||
<app-chart-sidebar></app-chart-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-status-bar></app-status-bar>
|
||||
@@ -0,0 +1,16 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.ui.celled.grid {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#sidebar-column {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ui.celled.grid > .row > .column {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="ui fluid card">
|
||||
<div class="ui placeholder">
|
||||
<div class="square image"></div>
|
||||
</div>
|
||||
<div class="ui fluid selection dropdown">
|
||||
<input type="hidden" name="Chart">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Chart</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="1">Chart 1</div>
|
||||
<div class="item" data-value="0">Chart 2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="textPanel" class="content">
|
||||
<span class="header">Funknitium-99</span>
|
||||
<div class="meta">
|
||||
<span>Fearofdark</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui positive buttons">
|
||||
<div class="ui button">Download Latest</div>
|
||||
<div id="versionButton" class="ui floating dropdown icon button">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<div class="item">Version 1</div>
|
||||
<div class="item">Version 2</div>
|
||||
<div class="item">Version 3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.ui.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#textPanel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#versionButton {
|
||||
max-width: 30px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component, AfterViewInit } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-sidebar',
|
||||
templateUrl: './chart-sidebar.component.html',
|
||||
styleUrls: ['./chart-sidebar.component.scss']
|
||||
})
|
||||
export class ChartSidebarComponent implements AfterViewInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
$('.ui.dropdown').dropdown()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<table class="ui unstackable selectable single line striped celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="collapsing">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox">
|
||||
</div>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Artist</th>
|
||||
<th>Album</th>
|
||||
<th>Genre</th>
|
||||
<th>Year</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let result of results">
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox">
|
||||
</div>
|
||||
</td>
|
||||
<td>{{result.name}}</td>
|
||||
<td>{{result.artist}}</td>
|
||||
<td>{{result.album}}</td>
|
||||
<td>{{result.genre}}</td>
|
||||
<td>{{result.year}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,4 @@
|
||||
.ui.checkbox {
|
||||
display: block;
|
||||
max-width: 17px;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component, AfterViewInit, Input } from '@angular/core'
|
||||
import { SongResult } from 'src/electron/shared/interfaces/search.interface'
|
||||
|
||||
@Component({
|
||||
selector: 'app-result-table',
|
||||
templateUrl: './result-table.component.html',
|
||||
styleUrls: ['./result-table.component.scss']
|
||||
})
|
||||
export class ResultTableComponent implements AfterViewInit {
|
||||
@Input() results: SongResult[]
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
$('.ui.checkbox').checkbox()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="ui bottom attached borderless menu">
|
||||
<div class="item">
|
||||
<!-- TODO: refactor this html into multiple sub-components -->
|
||||
<!-- TODO: add advanced search -->
|
||||
<div class="ui icon input">
|
||||
<input #searchBox type="text" placeholder=" Search..." (keyup.enter)="onSearch(searchBox.value)">
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div #typeDropdown class="ui item dropdown">
|
||||
Type <i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<!-- TODO: revise what items are displayed -->
|
||||
<a class="item">Any</a>
|
||||
<a class="item">Name</a>
|
||||
<a class="item">Artist</a>
|
||||
<a class="item">Album</a>
|
||||
<a class="item">Genre</a>
|
||||
<a class="item">Year</a>
|
||||
<a class="item">Charter</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<button class="ui positive disabled button">Bulk Download</button>
|
||||
</div>
|
||||
</div>
|
||||
24
src/app/components/browse/search-bar/search-bar.component.ts
Normal file
24
src/app/components/browse/search-bar/search-bar.component.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component, AfterViewInit, Output, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from 'src/app/core/services/electron.service'
|
||||
import { SearchType, SongResult } from 'src/electron/shared/interfaces/search.interface'
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-bar',
|
||||
templateUrl: './search-bar.component.html',
|
||||
styleUrls: ['./search-bar.component.scss']
|
||||
})
|
||||
export class SearchBarComponent implements AfterViewInit {
|
||||
@Output() resultsUpdated = new EventEmitter<SongResult[]>()
|
||||
|
||||
constructor(private electronService: ElectronService) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
$('.ui.dropdown').dropdown()
|
||||
}
|
||||
|
||||
async onSearch(query: string) {
|
||||
const results = await this.electronService.invoke('song-search', { query, type: SearchType.Any })
|
||||
|
||||
this.resultsUpdated.emit(results)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="ui bottom borderless menu">
|
||||
<div class="item">42 Results</div>
|
||||
<div class="item">
|
||||
<button class="ui positive button">Download Selected</button>
|
||||
</div>
|
||||
|
||||
<a class="item right">
|
||||
<div #progressBar class="ui progress">
|
||||
<div class="bar">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
.ui.progress {
|
||||
margin: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
12
src/app/components/browse/status-bar/status-bar.component.ts
Normal file
12
src/app/components/browse/status-bar/status-bar.component.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-bar',
|
||||
templateUrl: './status-bar.component.html',
|
||||
styleUrls: ['./status-bar.component.scss']
|
||||
})
|
||||
export class StatusBarComponent {
|
||||
|
||||
constructor() { }
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="ui top fixed menu">
|
||||
<div class="ui top menu">
|
||||
<a class="item" routerLinkActive="active" routerLink="/browse">Browse</a>
|
||||
<a class="item" routerLinkActive="active" routerLink="/library">Library</a>
|
||||
<a class="item" routerLinkActive="active" routerLink="/settings">Settings</a>
|
||||
|
||||
2
src/assets/semantic/jqueryImport.js
vendored
2
src/assets/semantic/jqueryImport.js
vendored
@@ -1,2 +1,2 @@
|
||||
// @ts-nocheck BS that is required for semantic to work
|
||||
window.jQuery = require('jquery')
|
||||
window.jQuery = $ = require('jquery')
|
||||
31
src/electron/ipc/SearchHandler.ipc.ts
Normal file
31
src/electron/ipc/SearchHandler.ipc.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IPCHandler } from '../shared/IPCHandler'
|
||||
import Database from '../shared/Database'
|
||||
import { SongSearch, SearchType, SongResult } from '../shared/interfaces/search.interface'
|
||||
import { escape } from 'mysql'
|
||||
|
||||
export default class SearchHandler implements IPCHandler<'song-search'> {
|
||||
event: 'song-search' = 'song-search'
|
||||
// TODO: add method documentation
|
||||
|
||||
async handler(search: SongSearch) {
|
||||
const db = await Database.getInstance()
|
||||
|
||||
return db.sendQuery(this.getSearchQuery(search)) as Promise<SongResult[]>
|
||||
}
|
||||
|
||||
private getSearchQuery(search: SongSearch) {
|
||||
switch(search.type) {
|
||||
case SearchType.Any: return this.getGeneralSearchQuery(search.query)
|
||||
default: return '<<<ERROR>>>' // TODO: add more search types
|
||||
}
|
||||
}
|
||||
|
||||
private getGeneralSearchQuery(searchString: string) {
|
||||
return `
|
||||
SELECT id, name, artist, album, genre, year
|
||||
FROM Song
|
||||
WHERE MATCH (name,artist,album,genre) AGAINST (${escape(searchString)}) > 0
|
||||
LIMIT ${20} OFFSET ${0};
|
||||
` // TODO: add parameters for the limit and offset
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IPCHandler } from '../shared/IPCHandler'
|
||||
import { TestInput } from '../shared/interfaces/test.interface'
|
||||
|
||||
export default class TestHandler implements IPCHandler<'test-event-A'> {
|
||||
event = 'test-event-A' as 'test-event-A'
|
||||
async handler(data: TestInput) {
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), 3000))
|
||||
|
||||
return `Processed data with value1 = ${data.value1} and value2 + 5 = ${data.value2 + 5}`
|
||||
}
|
||||
}
|
||||
74
src/electron/shared/Database.ts
Normal file
74
src/electron/shared/Database.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Connection, createConnection } from 'mysql'
|
||||
import { failQuery } from './ErrorMessages'
|
||||
|
||||
export default class Database {
|
||||
|
||||
// Singleton
|
||||
private static database: Database
|
||||
private constructor() { }
|
||||
static async getInstance() {
|
||||
if (this.database == undefined) {
|
||||
this.database = new Database()
|
||||
}
|
||||
await this.database.initDatabaseConnection()
|
||||
return this.database
|
||||
}
|
||||
|
||||
private conn: Connection
|
||||
|
||||
/**
|
||||
* Constructs a database connection to the chartmanager database.
|
||||
*/
|
||||
private async initDatabaseConnection() {
|
||||
this.conn = createConnection({
|
||||
host: 'chartmanager.cdtrqnlcxz86.us-east-1.rds.amazonaws.com',
|
||||
port: 3306,
|
||||
user: 'standarduser',
|
||||
password: 'E4OZXWDPiX9exUpMhcQq', // Note: this login is read-only
|
||||
database: 'chartmanagerdatabase',
|
||||
multipleStatements: true,
|
||||
charset: 'utf8mb4',
|
||||
typeCast: (field, next) => { // Convert 1/0 to true/false
|
||||
if (field.type == 'TINY' && field.length == 1) {
|
||||
return (field.string() == '1') // 1 = true, 0 = false
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.conn.connect(err => {
|
||||
if (err) {
|
||||
reject(`Failed to connect to database: ${err}`)
|
||||
return
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends <query> to the database.
|
||||
* @param query The query string to be sent.
|
||||
* @param queryStatement The nth response statement to be returned.
|
||||
* @returns one of the responses as type <ResponseType[]>, or an empty array if the query fails.
|
||||
*/
|
||||
async sendQuery<ResponseType>(query: string, queryStatement?: number) {
|
||||
return new Promise<ResponseType[]>(resolve => {
|
||||
this.conn.query(query, (err, results) => {
|
||||
if (err) {
|
||||
failQuery(query, err)
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
if (queryStatement !== undefined) {
|
||||
resolve(results[queryStatement - 1])
|
||||
} else {
|
||||
resolve(results)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
124
src/electron/shared/ErrorMessages.ts
Normal file
124
src/electron/shared/ErrorMessages.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { red } from 'cli-color'
|
||||
import { getRelativeFilepath } from './UtilFunctions'
|
||||
|
||||
// TODO: add better error reporting (through the UI)
|
||||
|
||||
/**
|
||||
* Displays an error message for reading files in the song folder
|
||||
*/
|
||||
export function failReadRelative(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to read files inside song folder (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for reading files
|
||||
*/
|
||||
export function failRead(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to read files inside song folder (${filepath}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for writing files
|
||||
*/
|
||||
export function failWrite(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to write to file (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for opening files
|
||||
*/
|
||||
export function failOpen(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to open file (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for deleting folders
|
||||
*/
|
||||
export function failDelete(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to delete folder (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for opening text files
|
||||
*/
|
||||
export function failEncoding(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to read text file (${getRelativeFilepath(filepath)
|
||||
}):\nJavaScript cannot parse using the detected text encoding of (${error})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for failing to parse an .ini file
|
||||
*/
|
||||
export function failParse(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to parse ini file (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for processing a query
|
||||
*/
|
||||
export function failQuery(query: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to execute query:\n${query}\nWith error:\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for connecting to the database
|
||||
*/
|
||||
export function failDatabase(error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to connect to database:\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for connecting to the database
|
||||
*/
|
||||
export function failTimeout(type: string, url: string, maxAttempts: number) {
|
||||
console.error(`${red('ERROR:')} Failed to connect to download server at (\n${url}) after ${maxAttempts} retry attempts. [type=${type}]`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for connecting to the database
|
||||
*/
|
||||
export function failResponse(statusCode: string, url: string) {
|
||||
console.error(`${red('ERROR:')} Failed to connect to download server at (\n${url}) [statusCode=${statusCode}]`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for processing audio files
|
||||
*/
|
||||
export function failFFMPEG(audioFile: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to process audio file (${getRelativeFilepath(audioFile)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for failing to create multiple threads
|
||||
*/
|
||||
export function failMultithread(error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to create multiple threads:\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for replacing files
|
||||
*/
|
||||
export function failReplace(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to rewrite file (${getRelativeFilepath(filepath)}):\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for downloading charts
|
||||
*/
|
||||
export function failDownload(error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to download chart:\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for reading files
|
||||
*/
|
||||
export function failScan(filepath: string) {
|
||||
console.error(`${red('ERROR:')} The specified library folder contains no files (${filepath})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message for failing to unzip an archived file
|
||||
*/
|
||||
export function failUnzip(filepath: string, error: any) {
|
||||
console.error(`${red('ERROR:')} Failed to extract archive at (${filepath}):\n${error}`)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import TestHandler from '../ipc/TestHandler.ipc'
|
||||
import { TestInput } from './interfaces/test.interface'
|
||||
import SearchHandler from '../ipc/SearchHandler.ipc'
|
||||
import { SongSearch, SongResult } from './interfaces/search.interface'
|
||||
|
||||
/**
|
||||
* To add a new IPC listener:
|
||||
@@ -11,14 +11,14 @@ import { TestInput } from './interfaces/test.interface'
|
||||
|
||||
export function getIPCHandlers(): IPCHandler<keyof IPCEvents>[] {
|
||||
return [
|
||||
new TestHandler()
|
||||
new SearchHandler()
|
||||
]
|
||||
}
|
||||
|
||||
export type IPCEvents = {
|
||||
['test-event-A']: {
|
||||
input: TestInput
|
||||
output: string
|
||||
['song-search']: {
|
||||
input: SongSearch
|
||||
output: SongResult[]
|
||||
}
|
||||
['test-event-B']: {
|
||||
input: number
|
||||
|
||||
19
src/electron/shared/Settings.ts
Normal file
19
src/electron/shared/Settings.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export class Settings {
|
||||
|
||||
// Singleton
|
||||
private constructor() { }
|
||||
private static settings: Settings
|
||||
static async getInstance() {
|
||||
if (this.settings == undefined) {
|
||||
this.settings = new Settings()
|
||||
}
|
||||
await this.settings.initSettings()
|
||||
return this.settings
|
||||
}
|
||||
|
||||
songsFolderPath: string
|
||||
|
||||
private async initSettings() {
|
||||
// TODO: load settings from settings file or set defaults
|
||||
}
|
||||
}
|
||||
15
src/electron/shared/UtilFunctions.ts
Normal file
15
src/electron/shared/UtilFunctions.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { basename } from 'path'
|
||||
import { Settings } from './Settings'
|
||||
|
||||
const settings = Settings.getInstance()
|
||||
|
||||
/**
|
||||
* @param absoluteFilepath The absolute filepath to a folder
|
||||
* @returns The relative filepath from the scanned folder to <absoluteFilepath>
|
||||
*/
|
||||
export function getRelativeFilepath(absoluteFilepath: string) {
|
||||
// TODO: figure out how these functions should use <settings> (like an async initialization script that
|
||||
// loads everything and connects to the database, etc...)
|
||||
// return basename(scanSettings.songsFolderPath) + absoluteFilepath.substring(scanSettings.songsFolderPath.length)
|
||||
return absoluteFilepath
|
||||
}
|
||||
17
src/electron/shared/interfaces/search.interface.ts
Normal file
17
src/electron/shared/interfaces/search.interface.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface SongSearch {
|
||||
query: string
|
||||
type: SearchType
|
||||
}
|
||||
|
||||
export enum SearchType {
|
||||
'Any', 'Name', 'Artist', 'Album', 'Genre', 'Year', 'Charter'
|
||||
}
|
||||
|
||||
export interface SongResult {
|
||||
id: number
|
||||
name: string
|
||||
artist: string
|
||||
album: string
|
||||
genre: string
|
||||
year: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface TestInput {
|
||||
value1: string
|
||||
value2: number
|
||||
}
|
||||
1
src/typings.d.ts
vendored
1
src/typings.d.ts
vendored
@@ -6,6 +6,7 @@ interface NodeModule {
|
||||
|
||||
// @ts-ignore
|
||||
declare var window: Window
|
||||
declare var $: any
|
||||
interface Window {
|
||||
process: any
|
||||
require: any
|
||||
|
||||
Reference in New Issue
Block a user