mirror of
https://github.com/litruv/AudioSort.git
synced 2026-07-24 02:36:01 +10:00
feat: enhance TagService to improve tag extraction and fallback logic;
update TagEditor for category filtering and styling; add middle click zoom reset in WaveformEditorCanvas; refine app.css for layout adjustments
This commit is contained in:
@@ -61,15 +61,19 @@ export class TagService {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const wave = new WaveFile(buffer);
|
||||
const tags = this.extractInfoTagMap(wave);
|
||||
|
||||
// Read from individual fields
|
||||
const title = tags.INAM?.trim() || undefined;
|
||||
const author = tags.IART?.trim() || undefined;
|
||||
const copyright = tags.ICOP?.trim() || undefined;
|
||||
|
||||
const ratingValue = tags.IRTD ? Number.parseInt(tags.IRTD, 10) : undefined;
|
||||
let rating: number | undefined;
|
||||
if (typeof ratingValue === 'number' && Number.isFinite(ratingValue)) {
|
||||
rating = Math.floor(ratingValue / 2);
|
||||
}
|
||||
|
||||
const copyright = tags.ICOP?.trim() || undefined;
|
||||
|
||||
// Try to read parentId from JSON comment first
|
||||
// Try to read parentId from JSON comment
|
||||
let parentId: number | undefined;
|
||||
const comment = tags.ICMT?.trim();
|
||||
if (comment) {
|
||||
@@ -79,17 +83,12 @@ export class TagService {
|
||||
parentId = parsed.parentId;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON or invalid, try fallback to IPAR
|
||||
const parentRaw = tags.IPAR?.trim();
|
||||
if (parentRaw && parentRaw.length > 0) {
|
||||
const parsedNum = Number.parseInt(parentRaw, 10);
|
||||
if (Number.isFinite(parsedNum)) {
|
||||
parentId = parsedNum;
|
||||
}
|
||||
}
|
||||
// Not JSON, ignore
|
||||
}
|
||||
} else {
|
||||
// No comment, try fallback to IPAR
|
||||
}
|
||||
|
||||
// Fallback to IPAR field if no JSON parentId
|
||||
if (parentId === undefined) {
|
||||
const parentRaw = tags.IPAR?.trim();
|
||||
if (parentRaw && parentRaw.length > 0) {
|
||||
const parsedNum = Number.parseInt(parentRaw, 10);
|
||||
@@ -100,8 +99,8 @@ export class TagService {
|
||||
}
|
||||
|
||||
return {
|
||||
author: tags.IART?.trim() || undefined,
|
||||
title: tags.INAM?.trim() || undefined,
|
||||
author,
|
||||
title,
|
||||
rating,
|
||||
copyright,
|
||||
parentId
|
||||
@@ -176,13 +175,7 @@ export class TagService {
|
||||
? String(metadata.parentId)
|
||||
: null;
|
||||
const commentPayload = {
|
||||
version: 1,
|
||||
tags: tagValuesList,
|
||||
categories: categoryValuesList,
|
||||
parentId: metadata.parentId ?? null,
|
||||
title: effectiveTitle,
|
||||
author: effectiveAuthor,
|
||||
rating: metadata.rating ?? null
|
||||
parentId: metadata.parentId ?? null
|
||||
} satisfies Record<string, unknown>;
|
||||
const commentText = JSON.stringify(commentPayload);
|
||||
|
||||
@@ -206,7 +199,7 @@ export class TagService {
|
||||
fs.writeFileSync(filePath, updatedBuffer);
|
||||
|
||||
console.log(`Wrote metadata to ${filePath}:`, {
|
||||
tags: Array.isArray(metadata.tags) ? metadata.tags.join(', ') : '',
|
||||
comment: commentText,
|
||||
categories: metadata.categories.join(', '),
|
||||
title: effectiveTitle,
|
||||
author: effectiveAuthor,
|
||||
|
||||
@@ -171,6 +171,14 @@ function App(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
await libraryStore.organizeFile(selectedFile.id, metadata);
|
||||
|
||||
// Scroll the file into view after organizing (filename may have changed)
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-file-id="${selectedFile.id}"]`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleUpdateCustomName = async (customName: string | null) => {
|
||||
|
||||
@@ -145,6 +145,7 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
||||
type="button"
|
||||
className={className}
|
||||
draggable={true}
|
||||
data-file-id={file.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
buttonRefs.current.set(file.id, node);
|
||||
|
||||
@@ -171,45 +171,6 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
||||
return (
|
||||
<section className="tag-editor">
|
||||
{showHeading && <h2>Categories</h2>}
|
||||
<div className="category-filter">
|
||||
<input
|
||||
value={categoryFilter}
|
||||
onChange={(event) => setCategoryFilter(event.target.value)}
|
||||
placeholder="Filter UCS categories"
|
||||
/>
|
||||
</div>
|
||||
{categoryFilter.trim().length > 0 && (
|
||||
filterSuggestions.length > 0 ? (
|
||||
<div className="category-filter-suggestions">
|
||||
<span className="category-filter-suggestions-label">Suggestions</span>
|
||||
<div className="category-filter-suggestion-list">
|
||||
{filterSuggestions.map((entry) => {
|
||||
const selected = selectedCategories.has(entry.id);
|
||||
const colorStyle = createCategoryStyleVars(categoryColorMap.get(entry.id));
|
||||
const categoryLabel = formatCategoryLabel(entry.category);
|
||||
const subCategoryLabel = formatCategoryLabel(entry.subCategory);
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className={selected ? 'category-filter-suggestion category-filter-suggestion--selected' : 'category-filter-suggestion'}
|
||||
onClick={() => toggleCategory(entry.id)}
|
||||
style={colorStyle}
|
||||
title={`${categoryLabel} ${subCategoryLabel}`}
|
||||
>
|
||||
<span className="category-filter-suggestion-name">
|
||||
<span className="category-label category-label--muted">{categoryLabel}</span>
|
||||
<span className="category-label category-label--primary">{subCategoryLabel}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="category-filter-empty">No categories match this filter.</div>
|
||||
)
|
||||
)}
|
||||
<div className="selected-categories">
|
||||
<div className="selected-category-chip-list">
|
||||
{selectedCategoryRecords.length === 0 && <span className="selected-category-empty">No categories selected</span>}
|
||||
@@ -235,6 +196,12 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
||||
star.style.width = 'auto';
|
||||
star.style.padding = '0 4px';
|
||||
}
|
||||
const removeBtn = e.currentTarget.querySelector('.selected-category-chip-remove-button') as HTMLElement;
|
||||
if (removeBtn) {
|
||||
removeBtn.style.opacity = '1';
|
||||
removeBtn.style.width = 'auto';
|
||||
removeBtn.style.padding = '0 4px';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const star = e.currentTarget.querySelector('.category-star-button') as HTMLElement;
|
||||
@@ -243,6 +210,12 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
||||
star.style.width = '0';
|
||||
star.style.padding = '0';
|
||||
}
|
||||
const removeBtn = e.currentTarget.querySelector('.selected-category-chip-remove-button') as HTMLElement;
|
||||
if (removeBtn) {
|
||||
removeBtn.style.opacity = '0';
|
||||
removeBtn.style.width = '0';
|
||||
removeBtn.style.padding = '0';
|
||||
}
|
||||
}}
|
||||
title={isPrimary ? `${categoryLabel} ${subCategoryLabel} (Primary - used for organization)` : `${categoryLabel} ${subCategoryLabel}`}
|
||||
>
|
||||
@@ -286,10 +259,16 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '0 4px',
|
||||
padding: '0',
|
||||
margin: 0,
|
||||
cursor: 'pointer',
|
||||
fontSize: '18px',
|
||||
lineHeight: 1
|
||||
lineHeight: 1,
|
||||
opacity: 0,
|
||||
width: '0',
|
||||
overflow: 'hidden',
|
||||
transition: 'opacity 0.2s ease, width 0.2s ease, padding 0.2s ease',
|
||||
flexShrink: 0
|
||||
}}
|
||||
aria-label="Remove category"
|
||||
>
|
||||
@@ -308,6 +287,45 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<div className="category-filter">
|
||||
<input
|
||||
value={categoryFilter}
|
||||
onChange={(event) => setCategoryFilter(event.target.value)}
|
||||
placeholder="Filter UCS categories"
|
||||
/>
|
||||
</div>
|
||||
{categoryFilter.trim().length > 0 && (
|
||||
filterSuggestions.length > 0 ? (
|
||||
<div className="category-filter-suggestions">
|
||||
<span className="category-filter-suggestions-label">Suggestions</span>
|
||||
<div className="category-filter-suggestion-list">
|
||||
{filterSuggestions.map((entry) => {
|
||||
const selected = selectedCategories.has(entry.id);
|
||||
const colorStyle = createCategoryStyleVars(categoryColorMap.get(entry.id));
|
||||
const categoryLabel = formatCategoryLabel(entry.category);
|
||||
const subCategoryLabel = formatCategoryLabel(entry.subCategory);
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className={selected ? 'category-filter-suggestion category-filter-suggestion--selected' : 'category-filter-suggestion'}
|
||||
onClick={() => toggleCategory(entry.id)}
|
||||
style={colorStyle}
|
||||
title={`${categoryLabel} ${subCategoryLabel}`}
|
||||
>
|
||||
<span className="category-filter-suggestion-name">
|
||||
<span className="category-label category-label--muted">{categoryLabel}</span>
|
||||
<span className="category-label category-label--primary">{subCategoryLabel}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="category-filter-empty">No categories match this filter.</div>
|
||||
)
|
||||
)}
|
||||
<div className="category-list-pane">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -415,6 +415,13 @@ export function WaveformEditorCanvas({
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: ReactPointerEvent<HTMLCanvasElement>) => {
|
||||
// Middle click - reset zoom to full view
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
updateViewport(0, durationMs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button === 2) {
|
||||
clickContextRef.current = null;
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
@@ -802,11 +802,11 @@ body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
gap: 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
background: rgba(12, 16, 22, 0.65);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.selected-category-chip-list {
|
||||
@@ -825,21 +825,20 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
border-radius: 999px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--category-color-base, rgba(39, 110, 241, 0.5));
|
||||
background: var(--category-color-soft, rgba(39, 110, 241, 0.2));
|
||||
color: var(--category-color-text, inherit);
|
||||
padding: 0.35rem 0.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
transition: background 0.15s ease;
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.selected-category-chip:hover {
|
||||
background: var(--category-color-strong, rgba(39, 110, 241, 0.35));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.selected-category-chip-label {
|
||||
|
||||
Reference in New Issue
Block a user