feat(12-02): replace config-page library section with full library management UI
- Add library list with name, path, track count per library - Add Library button opens folder picker, auto-creates library - Inline rename with Enter/Escape via overflow menu - Removal confirmation dialog with real impact counts (tracks, playlists, queue) - Toast notification with removal summary after library removal - Add GetAllLibrariesWithTrackCounts + Info type to backend Library struct - Add Wails binding stubs for AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact - Add Info, RemovalImpact, RemovalSummary types to models.ts - Remove old single-directory library config UI (GetLibraryDirectory/SetLibraryDirectory)
This commit is contained in:
@@ -443,3 +443,42 @@ func (l *Library) GetAllGenresWithCounts() (
|
|||||||
|
|
||||||
return genres, nil
|
return genres, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Info contains library metadata enriched with track count
|
||||||
|
// for the frontend settings UI.
|
||||||
|
type Info struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
TrackCount int64 `json:"trackCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllLibrariesWithTrackCounts returns all libraries with their
|
||||||
|
// audio file counts. Typically 1-5 libraries so the loop is trivial.
|
||||||
|
func (l *Library) GetAllLibrariesWithTrackCounts() ([]Info, error) {
|
||||||
|
libs, err := l.db.Queries.GetAllLibraries(l.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get libraries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Info, 0, len(libs))
|
||||||
|
|
||||||
|
for _, lib := range libs {
|
||||||
|
count, countErr := l.db.Queries.CountAudioFilesByLibrary(l.ctx, lib.ID)
|
||||||
|
if countErr != nil {
|
||||||
|
l.logger.Error("could not count tracks for library",
|
||||||
|
"libraryID", lib.ID, "error", countErr)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, Info{
|
||||||
|
ID: lib.ID,
|
||||||
|
Name: lib.Name,
|
||||||
|
Path: lib.Path,
|
||||||
|
TrackCount: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,21 @@ import {
|
|||||||
CancelCurrentScan,
|
CancelCurrentScan,
|
||||||
CancelAllScans,
|
CancelAllScans,
|
||||||
ScanAllLibraries,
|
ScanAllLibraries,
|
||||||
|
ScanLibrary,
|
||||||
PauseScan,
|
PauseScan,
|
||||||
ResumeScan,
|
ResumeScan,
|
||||||
|
AddLibrary,
|
||||||
|
RenameLibrary,
|
||||||
|
RemoveLibrary,
|
||||||
|
GetRemovalImpact,
|
||||||
|
GetAllLibrariesWithTrackCounts,
|
||||||
} from '@go/library/Library';
|
} from '@go/library/Library';
|
||||||
import {
|
import {
|
||||||
GetLibraryDirectory,
|
|
||||||
SetLibraryDirectory,
|
|
||||||
GetScanConcurrency,
|
GetScanConcurrency,
|
||||||
SetScanConcurrency,
|
SetScanConcurrency,
|
||||||
} from '@go/config/Config';
|
} from '@go/config/Config';
|
||||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||||
|
import type { library } from '@go/models';
|
||||||
import { ThemeController } from '@store/controllers/theme-controller';
|
import { ThemeController } from '@store/controllers/theme-controller';
|
||||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
@@ -337,8 +342,15 @@ export class ConfigPage extends LitElement {
|
|||||||
@state() private playlists: playlist.Summary[] = [];
|
@state() private playlists: playlist.Summary[] = [];
|
||||||
|
|
||||||
// --- Library state ---
|
// --- Library state ---
|
||||||
@state() private libraryDirectory = '';
|
@state() private libraries: library.Info[] = [];
|
||||||
@state() private selectedDirectory = '';
|
@state() private editingLibraryId: number | null = null;
|
||||||
|
@state() private editingName = '';
|
||||||
|
@state() private removingLibraryId: number | null = null;
|
||||||
|
@state() private removalImpact: library.RemovalImpact | null = null;
|
||||||
|
@state() private isRemoving = false;
|
||||||
|
@state() private toastMessage = '';
|
||||||
|
@state() private toastVisible = false;
|
||||||
|
@state() private activeMenuId: number | null = null;
|
||||||
@state() private scanning = false;
|
@state() private scanning = false;
|
||||||
@state() private statusMessage = '';
|
@state() private statusMessage = '';
|
||||||
@state() private scanProgress: ScanProgress | null = null;
|
@state() private scanProgress: ScanProgress | null = null;
|
||||||
@@ -357,6 +369,7 @@ export class ConfigPage extends LitElement {
|
|||||||
existingAction: string;
|
existingAction: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
|
private toastTimer?: ReturnType<typeof setTimeout>;
|
||||||
private cancelScanStarted?: () => void;
|
private cancelScanStarted?: () => void;
|
||||||
private cancelScanProgress?: () => void;
|
private cancelScanProgress?: () => void;
|
||||||
private cancelScanComplete?: () => void;
|
private cancelScanComplete?: () => void;
|
||||||
@@ -365,6 +378,9 @@ export class ConfigPage extends LitElement {
|
|||||||
private cancelScanCancelled?: () => void;
|
private cancelScanCancelled?: () => void;
|
||||||
private cancelScanQueued?: () => void;
|
private cancelScanQueued?: () => void;
|
||||||
private cancelScanQueueDrained?: () => void;
|
private cancelScanQueueDrained?: () => void;
|
||||||
|
private cancelLibraryAdded?: () => void;
|
||||||
|
private cancelLibraryRenamed?: () => void;
|
||||||
|
private cancelLibraryRemoved?: () => void;
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
:host {
|
:host {
|
||||||
@@ -812,6 +828,182 @@ export class ConfigPage extends LitElement {
|
|||||||
color: var(--yj-accent, #ffd43b);
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Library management */
|
||||||
|
.library-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
padding: 0.6em 0.5em;
|
||||||
|
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-row:hover {
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-name {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-name:hover {
|
||||||
|
color: var(--yj-accent, #ffd43b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-path {
|
||||||
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
|
font-size: var(--yj-text-sm, 13px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-count {
|
||||||
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
|
font-size: var(--yj-text-sm, 13px);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
border: 1px solid var(--yj-accent, #ffd43b);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-btn:hover {
|
||||||
|
background: var(--yj-bg-overlay, #495057);
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: var(--yj-bg-surface, #2a2a2a);
|
||||||
|
border: 1px solid var(--yj-border, #444);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-item {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-size: var(--yj-text-sm, 13px);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-item:hover {
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-item--danger {
|
||||||
|
color: var(--yj-error, #e03131);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-item--danger:hover {
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--yj-error, #e03131) 10%,
|
||||||
|
var(--yj-bg-elevated, #343a40)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-library-btn {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--yj-bg-surface, #2a2a2a);
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
padding: 0.75em 1.5em;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
font-size: var(--yj-text-sm, 13px);
|
||||||
|
z-index: 2000;
|
||||||
|
border: 1px solid var(--yj-border, #444);
|
||||||
|
animation: toast-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%)
|
||||||
|
translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%)
|
||||||
|
translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Keyboard shortcuts section */
|
/* Keyboard shortcuts section */
|
||||||
.shortcut-category {
|
.shortcut-category {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -880,7 +1072,7 @@ export class ConfigPage extends LitElement {
|
|||||||
|
|
||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.loadLibraryConfig();
|
void this.loadLibraries();
|
||||||
void this.loadPlaylists();
|
void this.loadPlaylists();
|
||||||
this.scrollMode =
|
this.scrollMode =
|
||||||
localStorage.getItem(SCROLL_STORAGE_KEY) || 'hover';
|
localStorage.getItem(SCROLL_STORAGE_KEY) || 'hover';
|
||||||
@@ -917,6 +1109,20 @@ export class ConfigPage extends LitElement {
|
|||||||
Events.LibraryScanQueueDrained,
|
Events.LibraryScanQueueDrained,
|
||||||
this.handleScanQueueDrained,
|
this.handleScanQueueDrained,
|
||||||
);
|
);
|
||||||
|
this.cancelLibraryAdded = EventsOn(
|
||||||
|
Events.LibraryAdded,
|
||||||
|
() => void this.loadLibraries(),
|
||||||
|
);
|
||||||
|
this.cancelLibraryRenamed = EventsOn(
|
||||||
|
Events.LibraryRenamed,
|
||||||
|
() => void this.loadLibraries(),
|
||||||
|
);
|
||||||
|
this.cancelLibraryRemoved = EventsOn(
|
||||||
|
Events.LibraryRemoved,
|
||||||
|
() => void this.loadLibraries(),
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener('click', this.handleDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback(): void {
|
override disconnectedCallback(): void {
|
||||||
@@ -929,21 +1135,27 @@ export class ConfigPage extends LitElement {
|
|||||||
this.cancelScanCancelled?.();
|
this.cancelScanCancelled?.();
|
||||||
this.cancelScanQueued?.();
|
this.cancelScanQueued?.();
|
||||||
this.cancelScanQueueDrained?.();
|
this.cancelScanQueueDrained?.();
|
||||||
|
this.cancelLibraryAdded?.();
|
||||||
|
this.cancelLibraryRenamed?.();
|
||||||
|
this.cancelLibraryRemoved?.();
|
||||||
|
|
||||||
|
document.removeEventListener('click', this.handleDocumentClick);
|
||||||
|
|
||||||
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadLibraryConfig(): Promise<void> {
|
private async loadLibraries(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [dir, mode] = await Promise.all([
|
const [libs, mode] = await Promise.all([
|
||||||
GetLibraryDirectory(),
|
GetAllLibrariesWithTrackCounts(),
|
||||||
GetScanConcurrency(),
|
GetScanConcurrency(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.libraryDirectory = dir;
|
this.libraries = libs ?? [];
|
||||||
this.selectedDirectory = dir;
|
|
||||||
this.concurrencyMode = mode;
|
this.concurrencyMode = mode;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
'Failed to load library config:',
|
'Failed to load libraries:',
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1072,37 +1284,121 @@ export class ConfigPage extends LitElement {
|
|||||||
this.cancelMetrics = null;
|
this.cancelMetrics = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleDirectoryBrowse = async (): Promise<void> => {
|
private handleAddLibrary = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const dir = await DirectoryPicker();
|
const dir = await DirectoryPicker();
|
||||||
|
|
||||||
if (dir) {
|
if (dir) {
|
||||||
this.selectedDirectory = dir;
|
await AddLibrary(dir);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
'Directory picker failed:',
|
'Failed to add library:',
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleSaveDirectory = async (): Promise<void> => {
|
private handleStartRename = (id: number, name: string): void => {
|
||||||
if (!this.selectedDirectory) return;
|
this.editingLibraryId = id;
|
||||||
|
this.editingName = name;
|
||||||
|
this.activeMenuId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleRenameKeyDown = async (e: KeyboardEvent): Promise<void> => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.editingLibraryId !== null && this.editingName.trim()) {
|
||||||
try {
|
try {
|
||||||
await SetLibraryDirectory(
|
await RenameLibrary(this.editingLibraryId, this.editingName.trim());
|
||||||
this.selectedDirectory,
|
|
||||||
);
|
|
||||||
this.libraryDirectory =
|
|
||||||
this.selectedDirectory;
|
|
||||||
this.statusMessage =
|
|
||||||
'Library directory saved. A scan will start automatically if the directory changed.';
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.statusMessage = `Failed to save directory: ${err}`;
|
console.error('Failed to rename library:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editingLibraryId = null;
|
||||||
|
this.editingName = '';
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
this.editingLibraryId = null;
|
||||||
|
this.editingName = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleRenameInput = (e: InputEvent): void => {
|
||||||
|
this.editingName = (e.target as HTMLInputElement).value;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleRescanLibrary = (id: number): void => {
|
||||||
|
this.activeMenuId = null;
|
||||||
|
void ScanLibrary(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleRemoveClick = async (id: number): Promise<void> => {
|
||||||
|
this.activeMenuId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const impact = await GetRemovalImpact(id);
|
||||||
|
|
||||||
|
this.removalImpact = impact;
|
||||||
|
this.removingLibraryId = id;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get removal impact:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleConfirmRemove = async (): Promise<void> => {
|
||||||
|
if (this.removingLibraryId === null) return;
|
||||||
|
|
||||||
|
const id = this.removingLibraryId;
|
||||||
|
const lib = this.libraries.find((l) => l.id === id);
|
||||||
|
const libName = lib?.name ?? 'Library';
|
||||||
|
|
||||||
|
this.isRemoving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await RemoveLibrary(id);
|
||||||
|
|
||||||
|
this.removingLibraryId = null;
|
||||||
|
this.removalImpact = null;
|
||||||
|
this.isRemoving = false;
|
||||||
|
this.showToast(
|
||||||
|
`Removed '${libName}' (${summary?.tracksDeleted ?? 0} tracks deleted)`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.isRemoving = false;
|
||||||
|
console.error('Failed to remove library:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleCancelRemove = (): void => {
|
||||||
|
this.removingLibraryId = null;
|
||||||
|
this.removalImpact = null;
|
||||||
|
this.isRemoving = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
private toggleOverflowMenu = (id: number, e: Event): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.activeMenuId = this.activeMenuId === id ? null : id;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDocumentClick = (): void => {
|
||||||
|
if (this.activeMenuId !== null) {
|
||||||
|
this.activeMenuId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private showToast(message: string): void {
|
||||||
|
this.toastMessage = message;
|
||||||
|
this.toastVisible = true;
|
||||||
|
|
||||||
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
||||||
|
|
||||||
|
this.toastTimer = setTimeout(() => {
|
||||||
|
this.toastVisible = false;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
private handleConcurrencyChange = (
|
private handleConcurrencyChange = (
|
||||||
e: CustomEvent<ConfigFieldChangeEvent>,
|
e: CustomEvent<ConfigFieldChangeEvent>,
|
||||||
): void => {
|
): void => {
|
||||||
@@ -1456,13 +1752,6 @@ export class ConfigPage extends LitElement {
|
|||||||
// COMPUTED
|
// COMPUTED
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
private get directoryChanged(): boolean {
|
|
||||||
return (
|
|
||||||
this.selectedDirectory !==
|
|
||||||
this.libraryDirectory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get hasRescanPhases(): boolean {
|
private get hasRescanPhases(): boolean {
|
||||||
if (!this.metrics) return false;
|
if (!this.metrics) return false;
|
||||||
|
|
||||||
@@ -1901,36 +2190,88 @@ export class ConfigPage extends LitElement {
|
|||||||
// --- Library section ---
|
// --- Library section ---
|
||||||
|
|
||||||
private renderLibrarySection() {
|
private renderLibrarySection() {
|
||||||
|
const removingLib = this.libraries.find(
|
||||||
|
(l) => l.id === this.removingLibraryId,
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<config-section
|
<config-section
|
||||||
heading="Library"
|
heading="Libraries"
|
||||||
description="Configure your music library location and scanning behaviour."
|
description="Manage your music library folders. Each library is scanned independently."
|
||||||
>
|
>
|
||||||
<config-field
|
<div class="library-list">
|
||||||
.schema=${{
|
${this.libraries.map(
|
||||||
key: 'libraryDirectory',
|
(lib) => html`
|
||||||
label: 'Library Directory',
|
<div class="library-row">
|
||||||
description:
|
${this.editingLibraryId === lib.id
|
||||||
'The root directory containing your music files.',
|
|
||||||
type: 'directory' as const,
|
|
||||||
}}
|
|
||||||
.value=${this.selectedDirectory}
|
|
||||||
@config-browse=${this.handleDirectoryBrowse}
|
|
||||||
></config-field>
|
|
||||||
|
|
||||||
${this.directoryChanged
|
|
||||||
? html`
|
? html`
|
||||||
<div class="save-row">
|
<input
|
||||||
<button
|
class="edit-input"
|
||||||
class="btn-success"
|
type="text"
|
||||||
?disabled=${this.scanning}
|
.value=${this.editingName}
|
||||||
@click=${this.handleSaveDirectory}
|
@input=${this.handleRenameInput}
|
||||||
|
@keydown=${this.handleRenameKeyDown}
|
||||||
|
@click=${(e: Event) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<span
|
||||||
|
class="library-name"
|
||||||
|
@click=${() => this.handleStartRename(lib.id, lib.name)}
|
||||||
>
|
>
|
||||||
Save Directory
|
${lib.name}
|
||||||
|
</span>
|
||||||
|
`}
|
||||||
|
<span class="library-path">${lib.path}</span>
|
||||||
|
<span class="library-count">
|
||||||
|
${lib.trackCount} tracks
|
||||||
|
</span>
|
||||||
|
<div class="overflow-wrapper">
|
||||||
|
<button
|
||||||
|
class="overflow-btn"
|
||||||
|
@click=${(e: Event) => this.toggleOverflowMenu(lib.id, e)}
|
||||||
|
>
|
||||||
|
\u22EF
|
||||||
</button>
|
</button>
|
||||||
|
${this.activeMenuId === lib.id
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="overflow-menu"
|
||||||
|
@click=${(e: Event) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overflow-item"
|
||||||
|
@click=${() => this.handleStartRename(lib.id, lib.name)}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="overflow-item"
|
||||||
|
@click=${() => this.handleRescanLibrary(lib.id)}
|
||||||
|
>
|
||||||
|
Rescan
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="overflow-item overflow-item--danger"
|
||||||
|
@click=${() => void this.handleRemoveClick(lib.id)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn-primary add-library-btn"
|
||||||
|
@click=${this.handleAddLibrary}
|
||||||
|
>
|
||||||
|
Add Library
|
||||||
|
</button>
|
||||||
|
|
||||||
<config-field
|
<config-field
|
||||||
.schema=${{
|
.schema=${{
|
||||||
@@ -2143,7 +2484,57 @@ export class ConfigPage extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
|
${this.removingLibraryId !== null && this.removalImpact
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="cancel-dialog-overlay"
|
||||||
|
@click=${this.handleCancelRemove}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cancel-dialog"
|
||||||
|
@click=${(e: Event) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div class="cancel-dialog-title">
|
||||||
|
Remove Library
|
||||||
|
</div>
|
||||||
|
<div class="cancel-dialog-message">
|
||||||
|
Remove '${removingLib?.name}'?
|
||||||
|
This will delete
|
||||||
|
${this.removalImpact.trackCount}
|
||||||
|
tracks, affect
|
||||||
|
${this.removalImpact.playlistsAffected}
|
||||||
|
playlists, and remove
|
||||||
|
${this.removalImpact.queueItemCount}
|
||||||
|
queue items.
|
||||||
|
</div>
|
||||||
|
<div class="cancel-dialog-actions">
|
||||||
|
<button
|
||||||
|
class="btn-ghost"
|
||||||
|
?disabled=${this.isRemoving}
|
||||||
|
@click=${this.handleCancelRemove}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-danger"
|
||||||
|
?disabled=${this.isRemoving}
|
||||||
|
@click=${this.handleConfirmRemove}
|
||||||
|
>
|
||||||
|
${this.isRemoving
|
||||||
|
? html`<span class="spinner"></span> Removing\u2026`
|
||||||
|
: 'Remove'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</config-section>
|
</config-section>
|
||||||
|
|
||||||
|
${this.toastVisible
|
||||||
|
? html`<div class="toast">${this.toastMessage}</div>`
|
||||||
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
@@ -3,6 +3,8 @@
|
|||||||
import {library} from '../models';
|
import {library} from '../models';
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function AddLibrary(arg1:string):Promise<library.Info>;
|
||||||
|
|
||||||
export function CancelAllScans():Promise<void>;
|
export function CancelAllScans():Promise<void>;
|
||||||
|
|
||||||
export function CancelCurrentScan():Promise<void>;
|
export function CancelCurrentScan():Promise<void>;
|
||||||
@@ -13,6 +15,8 @@ export function FullRescan():Promise<library.ScanMetrics>;
|
|||||||
|
|
||||||
export function GetAlbumTracks(arg1:number):Promise<Array<library.Track>>;
|
export function GetAlbumTracks(arg1:number):Promise<Array<library.Track>>;
|
||||||
|
|
||||||
|
export function GetAllLibrariesWithTrackCounts():Promise<Array<library.Info>>;
|
||||||
|
|
||||||
export function GetAlbumsByArtist(arg1:number):Promise<Array<library.Album>>;
|
export function GetAlbumsByArtist(arg1:number):Promise<Array<library.Album>>;
|
||||||
|
|
||||||
export function GetAllAlbums():Promise<Array<library.Album>>;
|
export function GetAllAlbums():Promise<Array<library.Album>>;
|
||||||
@@ -23,6 +27,8 @@ export function GetAllGenresWithCounts():Promise<Array<library.GenreWithCount>>;
|
|||||||
|
|
||||||
export function GetAllTracks():Promise<Array<library.Track>>;
|
export function GetAllTracks():Promise<Array<library.Track>>;
|
||||||
|
|
||||||
|
export function GetRemovalImpact(arg1:number):Promise<library.RemovalImpact>;
|
||||||
|
|
||||||
export function GetScanQueueLength():Promise<number>;
|
export function GetScanQueueLength():Promise<number>;
|
||||||
|
|
||||||
export function GetTracksByGenre(arg1:string):Promise<Array<library.Track>>;
|
export function GetTracksByGenre(arg1:string):Promise<Array<library.Track>>;
|
||||||
@@ -33,6 +39,10 @@ export function IsScanPaused():Promise<boolean>;
|
|||||||
|
|
||||||
export function PauseScan():Promise<void>;
|
export function PauseScan():Promise<void>;
|
||||||
|
|
||||||
|
export function RemoveLibrary(arg1:number):Promise<library.RemovalSummary>;
|
||||||
|
|
||||||
|
export function RenameLibrary(arg1:number, arg2:string):Promise<void>;
|
||||||
|
|
||||||
export function QueuedLibraryNames():Promise<Array<string>>;
|
export function QueuedLibraryNames():Promise<Array<string>>;
|
||||||
|
|
||||||
export function ResumeScan():Promise<void>;
|
export function ResumeScan():Promise<void>;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function AddLibrary(arg1) {
|
||||||
|
return window['go']['library']['Library']['AddLibrary'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function CancelAllScans() {
|
export function CancelAllScans() {
|
||||||
return window['go']['library']['Library']['CancelAllScans']();
|
return window['go']['library']['Library']['CancelAllScans']();
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,10 @@ export function GetAlbumTracks(arg1) {
|
|||||||
return window['go']['library']['Library']['GetAlbumTracks'](arg1);
|
return window['go']['library']['Library']['GetAlbumTracks'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetAllLibrariesWithTrackCounts() {
|
||||||
|
return window['go']['library']['Library']['GetAllLibrariesWithTrackCounts']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetAlbumsByArtist(arg1) {
|
export function GetAlbumsByArtist(arg1) {
|
||||||
return window['go']['library']['Library']['GetAlbumsByArtist'](arg1);
|
return window['go']['library']['Library']['GetAlbumsByArtist'](arg1);
|
||||||
}
|
}
|
||||||
@@ -42,6 +50,10 @@ export function GetAllTracks() {
|
|||||||
return window['go']['library']['Library']['GetAllTracks']();
|
return window['go']['library']['Library']['GetAllTracks']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetRemovalImpact(arg1) {
|
||||||
|
return window['go']['library']['Library']['GetRemovalImpact'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetScanQueueLength() {
|
export function GetScanQueueLength() {
|
||||||
return window['go']['library']['Library']['GetScanQueueLength']();
|
return window['go']['library']['Library']['GetScanQueueLength']();
|
||||||
}
|
}
|
||||||
@@ -62,6 +74,14 @@ export function PauseScan() {
|
|||||||
return window['go']['library']['Library']['PauseScan']();
|
return window['go']['library']['Library']['PauseScan']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RemoveLibrary(arg1) {
|
||||||
|
return window['go']['library']['Library']['RemoveLibrary'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenameLibrary(arg1, arg2) {
|
||||||
|
return window['go']['library']['Library']['RenameLibrary'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function QueuedLibraryNames() {
|
export function QueuedLibraryNames() {
|
||||||
return window['go']['library']['Library']['QueuedLibraryNames']();
|
return window['go']['library']['Library']['QueuedLibraryNames']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,62 @@ export namespace library {
|
|||||||
this.Name = source["Name"];
|
this.Name = source["Name"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class Info {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
trackCount: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new Info(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.path = source["path"];
|
||||||
|
this.trackCount = source["trackCount"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class RemovalImpact {
|
||||||
|
trackCount: number;
|
||||||
|
playlistsAffected: number;
|
||||||
|
queueItemCount: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RemovalImpact(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.trackCount = source["trackCount"];
|
||||||
|
this.playlistsAffected = source["playlistsAffected"];
|
||||||
|
this.queueItemCount = source["queueItemCount"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class RemovalSummary {
|
||||||
|
tracksDeleted: number;
|
||||||
|
artistsRemoved: number;
|
||||||
|
albumsRemoved: number;
|
||||||
|
genresRemoved: number;
|
||||||
|
playlistsAffected: number;
|
||||||
|
queueItemsRemoved: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RemovalSummary(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.tracksDeleted = source["tracksDeleted"];
|
||||||
|
this.artistsRemoved = source["artistsRemoved"];
|
||||||
|
this.albumsRemoved = source["albumsRemoved"];
|
||||||
|
this.genresRemoved = source["genresRemoved"];
|
||||||
|
this.playlistsAffected = source["playlistsAffected"];
|
||||||
|
this.queueItemsRemoved = source["queueItemsRemoved"];
|
||||||
|
}
|
||||||
|
}
|
||||||
export class GenreWithCount {
|
export class GenreWithCount {
|
||||||
Name: string;
|
Name: string;
|
||||||
TrackCount: number;
|
TrackCount: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user