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 buffer = fs.readFileSync(filePath);
|
||||||
const wave = new WaveFile(buffer);
|
const wave = new WaveFile(buffer);
|
||||||
const tags = this.extractInfoTagMap(wave);
|
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;
|
const ratingValue = tags.IRTD ? Number.parseInt(tags.IRTD, 10) : undefined;
|
||||||
let rating: number | undefined;
|
let rating: number | undefined;
|
||||||
if (typeof ratingValue === 'number' && Number.isFinite(ratingValue)) {
|
if (typeof ratingValue === 'number' && Number.isFinite(ratingValue)) {
|
||||||
rating = Math.floor(ratingValue / 2);
|
rating = Math.floor(ratingValue / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyright = tags.ICOP?.trim() || undefined;
|
// Try to read parentId from JSON comment
|
||||||
|
|
||||||
// Try to read parentId from JSON comment first
|
|
||||||
let parentId: number | undefined;
|
let parentId: number | undefined;
|
||||||
const comment = tags.ICMT?.trim();
|
const comment = tags.ICMT?.trim();
|
||||||
if (comment) {
|
if (comment) {
|
||||||
@@ -79,17 +83,12 @@ export class TagService {
|
|||||||
parentId = parsed.parentId;
|
parentId = parsed.parentId;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not JSON or invalid, try fallback to IPAR
|
// Not JSON, ignore
|
||||||
const parentRaw = tags.IPAR?.trim();
|
|
||||||
if (parentRaw && parentRaw.length > 0) {
|
|
||||||
const parsedNum = Number.parseInt(parentRaw, 10);
|
|
||||||
if (Number.isFinite(parsedNum)) {
|
|
||||||
parentId = parsedNum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// No comment, try fallback to IPAR
|
|
||||||
|
// Fallback to IPAR field if no JSON parentId
|
||||||
|
if (parentId === undefined) {
|
||||||
const parentRaw = tags.IPAR?.trim();
|
const parentRaw = tags.IPAR?.trim();
|
||||||
if (parentRaw && parentRaw.length > 0) {
|
if (parentRaw && parentRaw.length > 0) {
|
||||||
const parsedNum = Number.parseInt(parentRaw, 10);
|
const parsedNum = Number.parseInt(parentRaw, 10);
|
||||||
@@ -100,8 +99,8 @@ export class TagService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author: tags.IART?.trim() || undefined,
|
author,
|
||||||
title: tags.INAM?.trim() || undefined,
|
title,
|
||||||
rating,
|
rating,
|
||||||
copyright,
|
copyright,
|
||||||
parentId
|
parentId
|
||||||
@@ -176,13 +175,7 @@ export class TagService {
|
|||||||
? String(metadata.parentId)
|
? String(metadata.parentId)
|
||||||
: null;
|
: null;
|
||||||
const commentPayload = {
|
const commentPayload = {
|
||||||
version: 1,
|
parentId: metadata.parentId ?? null
|
||||||
tags: tagValuesList,
|
|
||||||
categories: categoryValuesList,
|
|
||||||
parentId: metadata.parentId ?? null,
|
|
||||||
title: effectiveTitle,
|
|
||||||
author: effectiveAuthor,
|
|
||||||
rating: metadata.rating ?? null
|
|
||||||
} satisfies Record<string, unknown>;
|
} satisfies Record<string, unknown>;
|
||||||
const commentText = JSON.stringify(commentPayload);
|
const commentText = JSON.stringify(commentPayload);
|
||||||
|
|
||||||
@@ -206,7 +199,7 @@ export class TagService {
|
|||||||
fs.writeFileSync(filePath, updatedBuffer);
|
fs.writeFileSync(filePath, updatedBuffer);
|
||||||
|
|
||||||
console.log(`Wrote metadata to ${filePath}:`, {
|
console.log(`Wrote metadata to ${filePath}:`, {
|
||||||
tags: Array.isArray(metadata.tags) ? metadata.tags.join(', ') : '',
|
comment: commentText,
|
||||||
categories: metadata.categories.join(', '),
|
categories: metadata.categories.join(', '),
|
||||||
title: effectiveTitle,
|
title: effectiveTitle,
|
||||||
author: effectiveAuthor,
|
author: effectiveAuthor,
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ function App(): JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await libraryStore.organizeFile(selectedFile.id, metadata);
|
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) => {
|
const handleUpdateCustomName = async (customName: string | null) => {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, sea
|
|||||||
type="button"
|
type="button"
|
||||||
className={className}
|
className={className}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
|
data-file-id={file.id}
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
if (node) {
|
if (node) {
|
||||||
buttonRefs.current.set(file.id, node);
|
buttonRefs.current.set(file.id, node);
|
||||||
|
|||||||
@@ -171,45 +171,6 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
|||||||
return (
|
return (
|
||||||
<section className="tag-editor">
|
<section className="tag-editor">
|
||||||
{showHeading && <h2>Categories</h2>}
|
{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-categories">
|
||||||
<div className="selected-category-chip-list">
|
<div className="selected-category-chip-list">
|
||||||
{selectedCategoryRecords.length === 0 && <span className="selected-category-empty">No categories selected</span>}
|
{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.width = 'auto';
|
||||||
star.style.padding = '0 4px';
|
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) => {
|
onMouseLeave={(e) => {
|
||||||
const star = e.currentTarget.querySelector('.category-star-button') as HTMLElement;
|
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.width = '0';
|
||||||
star.style.padding = '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}`}
|
title={isPrimary ? `${categoryLabel} ${subCategoryLabel} (Primary - used for organization)` : `${categoryLabel} ${subCategoryLabel}`}
|
||||||
>
|
>
|
||||||
@@ -286,10 +259,16 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
|||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: '0 4px',
|
padding: '0',
|
||||||
|
margin: 0,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '18px',
|
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"
|
aria-label="Remove category"
|
||||||
>
|
>
|
||||||
@@ -308,6 +287,45 @@ export function TagEditor({ categories, availableCategories, onSave, showHeading
|
|||||||
Clear All
|
Clear All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="category-list-pane">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -415,6 +415,13 @@ export function WaveformEditorCanvas({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (event: ReactPointerEvent<HTMLCanvasElement>) => {
|
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) {
|
if (event.button === 2) {
|
||||||
clickContextRef.current = null;
|
clickContextRef.current = null;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
|
|||||||
@@ -802,11 +802,11 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: none;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
padding: 0.65rem 0.75rem;
|
padding: 0;
|
||||||
background: rgba(12, 16, 22, 0.65);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-category-chip-list {
|
.selected-category-chip-list {
|
||||||
@@ -825,21 +825,20 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--category-color-base, rgba(39, 110, 241, 0.5));
|
border: 1px solid var(--category-color-base, rgba(39, 110, 241, 0.5));
|
||||||
background: var(--category-color-soft, rgba(39, 110, 241, 0.2));
|
background: var(--category-color-soft, rgba(39, 110, 241, 0.2));
|
||||||
color: var(--category-color-text, inherit);
|
color: var(--category-color-text, inherit);
|
||||||
padding: 0.35rem 0.8rem;
|
padding: 0.35rem 0.5rem;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease, transform 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-category-chip:hover {
|
.selected-category-chip:hover {
|
||||||
background: var(--category-color-strong, rgba(39, 110, 241, 0.35));
|
background: var(--category-color-strong, rgba(39, 110, 241, 0.35));
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-category-chip-label {
|
.selected-category-chip-label {
|
||||||
|
|||||||
Reference in New Issue
Block a user