Fix result table layout

This commit is contained in:
Geomitron
2023-12-14 13:36:45 -06:00
parent c2e355cb53
commit 742c6a28d0
23 changed files with 81 additions and 196 deletions

View File

@@ -81,6 +81,7 @@
"prettier": "^3.1.0", "prettier": "^3.1.0",
"prettier-eslint": "^16.1.2", "prettier-eslint": "^16.1.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }

12
pnpm-lock.yaml generated
View File

@@ -169,6 +169,9 @@ devDependencies:
source-map-support: source-map-support:
specifier: ^0.5.21 specifier: ^0.5.21
version: 0.5.21 version: 0.5.21
tailwind-scrollbar:
specifier: ^3.0.5
version: 3.0.5(tailwindcss@3.3.5)
tailwindcss: tailwindcss:
specifier: ^3.3.5 specifier: ^3.3.5
version: 3.3.5 version: 3.3.5
@@ -9983,6 +9986,15 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: true dev: true
/tailwind-scrollbar@3.0.5(tailwindcss@3.3.5):
resolution: {integrity: sha512-0ZwxTivevqq9BY9fRP9zDjHl7Tu+J5giBGbln+0O1R/7nHtBUKnjQcA1aTIhK7Oyjp6Uc/Dj6/dn8Dq58k5Uww==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: 3.x
dependencies:
tailwindcss: 3.3.5
dev: true
/tailwindcss@3.3.5: /tailwindcss@3.3.5:
resolution: {integrity: sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==} resolution: {integrity: sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}

View File

@@ -1,12 +1,12 @@
<app-search-bar></app-search-bar> <app-search-bar></app-search-bar>
<div class="ui celled two column grid"> <div class="flex flex-1 overflow-y-hidden">
<div id="table-row" class="row"> <!-- TODO: adjust sizing -->
<div id="table-column" class="column twelve wide"> <div
class="flex-grow-[1] overflow-y-auto scrollbar scrollbar-w-2 scrollbar-h-2 scrollbar-track-base-300 scrollbar-thumb-neutral scrollbar-thumb-rounded-full">
<app-result-table #resultTable (rowClicked)="chartSidebar.onRowClicked($event)"></app-result-table> <app-result-table #resultTable (rowClicked)="chartSidebar.onRowClicked($event)"></app-result-table>
</div> </div>
<div id="sidebar-column" class="column four wide"> <div class="flex-grow-[1] min-w-[175px]">
<app-chart-sidebar #chartSidebar></app-chart-sidebar> <app-chart-sidebar #chartSidebar></app-chart-sidebar>
</div> </div>
</div>
</div> </div>
<app-status-bar #statusBar></app-status-bar> <app-status-bar #statusBar></app-status-bar>

View File

@@ -1,27 +0,0 @@
:host {
display: contents;
}
.two.column.grid {
display: contents;
margin: 0;
}
#table-row {
flex-grow: 1;
min-height: 0;
flex-wrap: nowrap;
box-shadow: none;
}
#table-column {
overflow-y: auto;
padding: 0;
}
#sidebar-column {
display: flex;
min-width: 175px;
overflow: hidden;
padding: 0;
}

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core' import { Component, HostBinding, ViewChild } from '@angular/core'
import { SearchService } from 'src-angular/app/core/services/search.service' import { SearchService } from 'src-angular/app/core/services/search.service'
@@ -9,10 +9,9 @@ import { StatusBarComponent } from './status-bar/status-bar.component'
@Component({ @Component({
selector: 'app-browse', selector: 'app-browse',
templateUrl: './browse.component.html', templateUrl: './browse.component.html',
styleUrls: ['./browse.component.scss'],
}) })
export class BrowseComponent { export class BrowseComponent {
@HostBinding('class.contents') contents = true
@ViewChild('resultTable', { static: true }) resultTable: ResultTableComponent @ViewChild('resultTable', { static: true }) resultTable: ResultTableComponent
@ViewChild('chartSidebar', { static: true }) chartSidebar: ChartSidebarComponent @ViewChild('chartSidebar', { static: true }) chartSidebar: ChartSidebarComponent
@ViewChild('statusBar', { static: true }) statusBar: StatusBarComponent @ViewChild('statusBar', { static: true }) statusBar: StatusBarComponent

View File

@@ -1,10 +1,8 @@
<td> <td>
<div #checkbox class="ui checkbox" (click)="$event.stopPropagation()"> <input #checkAllCheckbox type="checkbox" class="checkbox" (click)="$event.stopPropagation()" [(ngModel)]="selected" />
<input type="checkbox" />
</div>
</td> </td>
<td> <td>
<span id="chartCount" *ngIf="song.length > 1">{{ song.length }}</span> {{ song[0].name }} <span *ngIf="song.length > 1" class="rounded-sm bg-accent text-accent-content px-1 mr-1 font-bold">{{ song.length }}</span> {{ song[0].name }}
</td> </td>
<td>{{ song[0].artist }}</td> <td>{{ song[0].artist }}</td>
<td>{{ song[0].album || 'Various' }}</td> <td>{{ song[0].album || 'Various' }}</td>

View File

@@ -1,10 +0,0 @@
.ui.checkbox {
display: block;
}
#chartCount {
background-color: lightgray;
border-radius: 3px;
padding: 1px 4px 2px 4px;
margin-right: 3px;
}

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core' import { Component, Input, OnInit } from '@angular/core'
import { ChartData } from '../../../../../../src-shared/interfaces/search.interface' import { ChartData } from '../../../../../../src-shared/interfaces/search.interface'
import { SelectionService } from '../../../../core/services/selection.service' import { SelectionService } from '../../../../core/services/selection.service'
@@ -6,36 +6,24 @@ import { SelectionService } from '../../../../core/services/selection.service'
@Component({ @Component({
selector: 'tr[app-result-table-row]', selector: 'tr[app-result-table-row]',
templateUrl: './result-table-row.component.html', templateUrl: './result-table-row.component.html',
styleUrls: ['./result-table-row.component.scss'],
}) })
export class ResultTableRowComponent implements AfterViewInit { export class ResultTableRowComponent implements OnInit {
@Input() song: ChartData[] @Input() song: ChartData[]
@ViewChild('checkbox', { static: true }) checkbox: ElementRef
constructor(private selectionService: SelectionService) { } constructor(private selectionService: SelectionService) { }
get songID() { ngOnInit() {
return this.song[0].songId ?? this.song[0].chartId this.selectionService.selections[this.groupId] = false
} }
ngAfterViewInit() { get groupId() {
this.selectionService.onSelectionChanged(this.songID, isChecked => { return this.song[0].groupId
if (isChecked) {
// TODO
// $(this.checkbox.nativeElement).checkbox('check')
} else {
// $(this.checkbox.nativeElement).checkbox('uncheck')
} }
})
// $(this.checkbox.nativeElement).checkbox({ get selected() {
// onChecked: () => { return this.selectionService.selections[this.groupId] ?? false
// this.selectionService.selectSong(this.songID) }
// }, set selected(value: boolean) {
// onUnchecked: () => { this.selectionService.selections[this.groupId] = value
// this.selectionService.deselectSong(this.songID)
// },
// })
} }
} }

View File

@@ -1,26 +1,26 @@
<table <table id="resultTable" class="table table-zebra table-pin-rows overflow-y-auto">
id="resultTable"
class="ui stackable selectable single sortable fixed line striped compact small table"
[class.inverted]="settingsService.theme === 'dark'">
<!-- TODO: maybe have some of these tags customizable? E.g. small/large/compact/padded --> <!-- TODO: maybe have some of these tags customizable? E.g. small/large/compact/padded -->
<!-- TODO: learn semantic themes in order to change the $mobileBreakpoint global variable (better search table adjustment) --> <!-- TODO: learn semantic themes in order to change the $mobileBreakpoint global variable (better search table adjustment) -->
<thead> <thead>
<!-- NOTE: it would be nice to make this header sticky, but Fomantic-UI doesn't currently support that --> <!-- NOTE: it would be nice to make this header sticky, but Fomantic-UI doesn't currently support that -->
<tr> <tr>
<th class="collapsing" id="checkboxColumn"> <th class="collapsing" id="checkboxColumn">
<div class="ui checkbox" id="checkbox" #checkboxColumn appCheckbox (checked)="checkAll($event)"> <input #checkAllCheckbox type="checkbox" class="checkbox" (change)="checkAll(checkAllCheckbox.checked)" />
<input type="checkbox" />
</div>
</th> </th>
<th class="four wide" [class.sorted]="sortColumn === 'name'" [ngClass]="sortDirection" (click)="onColClicked('name')">Name</th> <th [class.sorted]="sortColumn === 'name'" [ngClass]="sortDirection" (click)="onColClicked('name')">Name</th>
<th class="four wide" [class.sorted]="sortColumn === 'artist'" [ngClass]="sortDirection" (click)="onColClicked('artist')">Artist</th> <th [class.sorted]="sortColumn === 'artist'" [ngClass]="sortDirection" (click)="onColClicked('artist')">Artist</th>
<th class="four wide" [class.sorted]="sortColumn === 'album'" [ngClass]="sortDirection" (click)="onColClicked('album')">Album</th> <th [class.sorted]="sortColumn === 'album'" [ngClass]="sortDirection" (click)="onColClicked('album')">Album</th>
<th class="four wide" [class.sorted]="sortColumn === 'genre'" [ngClass]="sortDirection" (click)="onColClicked('genre')">Genre</th> <th [class.sorted]="sortColumn === 'genre'" [ngClass]="sortDirection" (click)="onColClicked('genre')">Genre</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (song of songs; track song) { @for (song of songs; track song) {
<tr app-result-table-row (click)="onRowClicked(song)" [class.active]="activeSong === song" [song]="song"></tr> <tr
app-result-table-row
(click)="onRowClicked(song)"
[class.!bg-neutral]="activeSong === song"
[class.!text-neutral-content]="activeSong === song"
[song]="song"></tr>
} }
</tbody> </tbody>
</table> </table>

View File

@@ -1,25 +0,0 @@
:host {
display: contents;
}
.ui.checkbox {
display: block;
}
#checkboxColumn {
width: 34.64px;
}
#resultTable {
border-radius: 0px;
thead>tr:first-child>th:first-child,
thead>tr:first-child>th:last-child {
border-radius: 0px;
}
th {
position: sticky;
top: 0;
z-index:1;
}
}

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core' import { Component, EventEmitter, HostBinding, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core'
import Comparators from 'comparators' import Comparators from 'comparators'
import { SettingsService } from 'src-angular/app/core/services/settings.service' import { SettingsService } from 'src-angular/app/core/services/settings.service'
@@ -12,9 +12,9 @@ import { ResultTableRowComponent } from './result-table-row/result-table-row.com
@Component({ @Component({
selector: 'app-result-table', selector: 'app-result-table',
templateUrl: './result-table.component.html', templateUrl: './result-table.component.html',
styleUrls: ['./result-table.component.scss'],
}) })
export class ResultTableComponent implements OnInit { export class ResultTableComponent implements OnInit {
@HostBinding('class.contents') contents = true
@Output() rowClicked = new EventEmitter<ChartData[]>() @Output() rowClicked = new EventEmitter<ChartData[]>()
@@ -32,10 +32,6 @@ export class ResultTableComponent implements OnInit {
) { } ) { }
ngOnInit() { ngOnInit() {
this.selectionService.onSelectAllChanged(selected => {
this.checkboxColumn.check(selected)
})
this.searchService.searchUpdated.subscribe(() => { this.searchService.searchUpdated.subscribe(() => {
this.activeSong = null this.activeSong = null
this.updateSort() this.updateSort()
@@ -76,6 +72,7 @@ export class ResultTableComponent implements OnInit {
* Called when the user checks the `checkboxColumn`. * Called when the user checks the `checkboxColumn`.
*/ */
checkAll(isChecked: boolean) { checkAll(isChecked: boolean) {
console.log(isChecked)
if (isChecked) { if (isChecked) {
this.selectionService.selectAll() this.selectionService.selectAll()
} else { } else {

View File

@@ -59,7 +59,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="collapse-content justify-center"> <div class="collapse-content justify-center pt-2">
<form [formGroup]="advancedSearchForm"> <form [formGroup]="advancedSearchForm">
<div class="flex flex-wrap gap-5 justify-center"> <div class="flex flex-wrap gap-5 justify-center">
<div> <div>

View File

@@ -1,4 +1,4 @@
<div id="bottomMenu" class="ui bottom borderless menu"> <div id="bottomMenu" class="ui bottom borderless menu border-t border-t-neutral">
<div *ngIf="(searchService.groupedSongs?.length ?? 0) > 0" class="item"> <div *ngIf="(searchService.groupedSongs?.length ?? 0) > 0" class="item">
{{ searchService.groupedSongs.length }}{{ allResultsVisible ? '' : '+' }} Result{{ searchService.groupedSongs.length === 1 ? '' : 's' }} {{ searchService.groupedSongs.length }}{{ allResultsVisible ? '' : '+' }} Result{{ searchService.groupedSongs.length === 1 ? '' : 's' }}
</div> </div>

View File

@@ -3,11 +3,6 @@
min-width: 200px; min-width: 200px;
} }
#bottomMenu {
border-radius: 0px;
border-width: 1px 0px 0px 0px;
}
#downloadsModal { #downloadsModal {
.header { .header {
display: flex; display: flex;

View File

@@ -102,9 +102,10 @@ export class StatusBarComponent {
} }
deselectSongsWithMultipleCharts() { deselectSongsWithMultipleCharts() {
for (const chartGroup of this.chartGroups) { // TODO
this.selectionService.deselectSong(chartGroup[0].songID) // for (const chartGroup of this.chartGroups) {
} // this.selectionService.deselectSong(chartGroup[0].songID)
// }
} }
clearCompleted() { clearCompleted() {

View File

@@ -5,6 +5,7 @@ import { FormControl } from '@angular/forms'
import { chain, xorBy } from 'lodash' import { chain, xorBy } from 'lodash'
import { catchError, mergeMap, tap, throwError, timer } from 'rxjs' import { catchError, mergeMap, tap, throwError, timer } from 'rxjs'
import { Difficulty, Instrument } from 'scan-chart' import { Difficulty, Instrument } from 'scan-chart'
import { environment } from 'src-angular/environments/environment'
import { AdvancedSearch, ChartData, SearchResult } from 'src-shared/interfaces/search.interface' import { AdvancedSearch, ChartData, SearchResult } from 'src-shared/interfaces/search.interface'
@Injectable({ @Injectable({
@@ -60,7 +61,7 @@ export class SearchService {
this.search().subscribe() this.search().subscribe()
} }
get areMorePages() { return this.songsResponse.page && this.groupedSongs.length === this.songsResponse.page * 20 } get areMorePages() { return this.songsResponse.page && this.groupedSongs.length === this.songsResponse.page * 10 }
/** /**
* General search, uses the `/search?q=` endpoint. * General search, uses the `/search?q=` endpoint.
@@ -80,8 +81,9 @@ export class SearchService {
} }
let retries = 10 let retries = 10
return this.http.post<SearchResult>(`/api/search`, { return this.http.post<SearchResult>(`${environment.apiUrl}/search`, {
search, search,
per_page: 25,
page: this.currentPage, page: this.currentPage,
instrument: this.instrument.value, instrument: this.instrument.value,
difficulty: this.difficulty.value, difficulty: this.difficulty.value,
@@ -125,7 +127,7 @@ export class SearchService {
this.isDefaultSearch = false this.isDefaultSearch = false
let retries = 10 let retries = 10
return this.http.post<{ data: SearchResult['data'] }>(`/api/search/advanced`, search).pipe( return this.http.post<{ data: SearchResult['data'] }>(`${environment.apiUrl}/search/advanced`, search).pipe(
catchError((err, caught) => { catchError((err, caught) => {
if (err.status === 400 || retries-- <= 0) { if (err.status === 400 || retries-- <= 0) {
this.searchLoading = false this.searchLoading = false

View File

@@ -1,6 +1,5 @@
import { EventEmitter, Injectable } from '@angular/core' import { EventEmitter, Injectable } from '@angular/core'
import { SearchResult } from '../../../../src-shared/interfaces/search.interface'
import { SearchService } from './search.service' import { SearchService } from './search.service'
// Note: this class prevents event cycles by only emitting events if the checkbox changes // Note: this class prevents event cycles by only emitting events if the checkbox changes
@@ -10,27 +9,13 @@ import { SearchService } from './search.service'
}) })
export class SelectionService { export class SelectionService {
private searchResults: Partial<SearchResult>
private selectAllChangedEmitter = new EventEmitter<boolean>() private selectAllChangedEmitter = new EventEmitter<boolean>()
private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {}
private allSelected = false public selections: { [groupId: number]: boolean | undefined } = {}
private selections: { [songID: number]: boolean | undefined } = {}
constructor(searchService: SearchService) { constructor(searchService: SearchService) {
searchService.searchUpdated.subscribe(results => { searchService.searchUpdated.subscribe(() => {
this.searchResults = results
if (this.allSelected) {
this.selectAll() // Select newly added rows if allSelected
}
})
searchService.searchUpdated.subscribe(results => {
this.searchResults = results
this.selectionChangedCallbacks = {}
this.selections = {} this.selections = {}
this.selectAllChangedEmitter.emit(false)
}) })
} }
@@ -44,46 +29,17 @@ export class SelectionService {
this.selectAllChangedEmitter.subscribe(callback) this.selectAllChangedEmitter.subscribe(callback)
} }
/**
* Emits an event when the selection for `songID` needs to change.
* (note: only one emitter can be registered per `songID`)
*/
onSelectionChanged(songID: number, callback: (selection: boolean) => void) {
this.selectionChangedCallbacks[songID] = callback
}
deselectAll() { deselectAll() {
if (this.allSelected) { for (const groupId in this.selections) {
this.allSelected = false this.selections[groupId] = false
}
this.selectAllChangedEmitter.emit(false) this.selectAllChangedEmitter.emit(false)
} }
// TODO
// setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
}
selectAll() { selectAll() {
if (!this.allSelected) { for (const groupId in this.selections) {
this.allSelected = true this.selections[groupId] = true
}
this.selectAllChangedEmitter.emit(true) this.selectAllChangedEmitter.emit(true)
} }
// TODO
// setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
}
deselectSong(songID: number) {
if (this.selections[songID]) {
this.selections[songID] = false
this.selectionChangedCallbacks[songID](false)
}
}
selectSong(songID: number) {
if (!this.selections[songID]) {
this.selections[songID] = true
this.selectionChangedCallbacks[songID](true)
}
}
} }

View File

@@ -1,3 +1,4 @@
export const environment = { export const environment = {
production: true, production: true,
apiUrl: 'https://api.enchor.us',
} }

View File

@@ -4,6 +4,7 @@
export const environment = { export const environment = {
production: false, production: false,
apiUrl: 'https://api.enchor.us',
} }
/* /*

View File

@@ -1,12 +1,12 @@
<!doctype html> <!doctype html>
<html lang="en" data-theme="dark" class="bg-base-300"> <html lang="en" data-theme="dark" class="bg-base-300 overflow-hidden">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Bridge</title> <title>Bridge</title>
<base href="./" /> <base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body> <body class="scrollbar">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@@ -7,12 +7,3 @@ export const libraryPath = join(dataPath, 'library.db')
export const settingsPath = join(dataPath, 'settings.json') export const settingsPath = join(dataPath, 'settings.json')
export const tempPath = join(dataPath, 'temp') export const tempPath = join(dataPath, 'temp')
export const themesPath = join(dataPath, 'themes') export const themesPath = join(dataPath, 'themes')
// URL
export const serverURL = 'bridge-db.net'
// OAuth callback server
export const SERVER_PORT = 42813
export const REDIRECT_BASE = `http://127.0.0.1:${SERVER_PORT}`
export const REDIRECT_PATH = `/oauth2callback`
export const REDIRECT_URI = `${REDIRECT_BASE}${REDIRECT_PATH}`

View File

@@ -131,6 +131,8 @@ export interface SearchResult {
chartId: number chartId: number
/** The unique database identifier for the song, or `null` if there is only one chart of the song. */ /** The unique database identifier for the song, or `null` if there is only one chart of the song. */
songId: number | null songId: number | null
/** The unique database identifier for the song, or (-versionGroupId) if there is only one chart of the song. */
groupId: number
/** The MD5 hash of the normalized album art file. */ /** The MD5 hash of the normalized album art file. */
albumArtMd5: string | null albumArtMd5: string | null
/** The MD5 hash of the chart folder or .sng file. */ /** The MD5 hash of the chart folder or .sng file. */

View File

@@ -8,7 +8,10 @@ module.exports = {
}, },
}, },
content: ['./src-angular/**/*.{html,js,ts}'], content: ['./src-angular/**/*.{html,js,ts}'],
plugins: [require('daisyui')], plugins: [
require('tailwind-scrollbar')({ nocompatible: true }),
require('daisyui'),
],
daisyui: { daisyui: {
logs: false, logs: false,
themes: [ themes: [