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:
2025-11-11 10:31:32 +11:00
parent 65ab2776eb
commit f4bea5e3fd
6 changed files with 98 additions and 72 deletions

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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"

View File

@@ -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;

View File

@@ -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 {