Skip to content

Commit aa8b07e

Browse files
committed
feat: v1.0.13 — compact list view, custom columns, persistent sort & Intel Mac builds
- macOS releases are now a universal binary (aarch64 + x86_64), so Stack runs on Intel Macs; CI installs both Rust targets and builds with --target universal-apple-darwin - Compact list view: 40px rows without the category line, smaller artwork and waveform; virtualizer estimate and column-header spacer track the same density - Column visibility via right-click on the column header — Waveform, Time, Key, BPM, Folder, plus compact rows; backed by Settings so it persists - New Folder column shows each sample's location relative to its pack and jumps to that folder on click - Sort order persists across launches (localStorage), and picking a new sort field now starts in its natural direction (names A-Z, recency newest-first) - Sidebar: clicking a folder opens and expands it, double-click collapses, and expanded folders are remembered between launches - Fixed the browser column header showing Waveform/Key/BPM labels while those columns were hidden - Removed dead SortControls component
1 parent afb86b3 commit aa8b07e

19 files changed

Lines changed: 297 additions & 85 deletions

‎.github/workflows/release.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ jobs:
1111
fail-fast: false
1212
matrix:
1313
include:
14+
# Universal binary so the DMG runs on both Apple Silicon and Intel Macs.
15+
# GitHub's macOS runners are arm64 only, so x86_64 is cross-compiled.
1416
- os: macos-latest
17+
rust-targets: aarch64-apple-darwin,x86_64-apple-darwin
18+
args: --target universal-apple-darwin
1519
- os: windows-latest
20+
rust-targets: ''
21+
args: ''
1622

1723
runs-on: ${{ matrix.os }}
1824
permissions:
@@ -29,6 +35,8 @@ jobs:
2935

3036
- name: Install Rust stable
3137
uses: dtolnay/rust-toolchain@stable
38+
with:
39+
targets: ${{ matrix.rust-targets }}
3240

3341
- name: Cache Rust build
3442
uses: Swatinem/rust-cache@v2
@@ -76,3 +84,4 @@ jobs:
7684
releaseBody: ${{ steps.notes.outputs.body }}
7785
releaseDraft: false
7886
prerelease: false
87+
args: ${{ matrix.args }}

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stack",
33
"private": true,
4-
"version": "1.0.12",
4+
"version": "1.0.13",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎src-tauri/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stack"
3-
version = "1.0.12"
3+
version = "1.0.13"
44
description = "Local sample library manager"
55
authors = ["Stack"]
66
edition = "2021"

‎src-tauri/src/models/settings.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ pub struct Settings {
4949
/// Stick the toolbar + column header to the top when scrolling inside a folder
5050
#[serde(default = "default_true")]
5151
pub sticky_toolbar: bool,
52+
/// Denser asset rows: smaller artwork, no category line
53+
#[serde(default)]
54+
pub compact_list: bool,
55+
/// Show the duration column on asset rows
56+
#[serde(default = "default_true")]
57+
pub show_time_badge: bool,
58+
/// Show the folder-path column on asset rows
59+
#[serde(default)]
60+
pub show_folder_column: bool,
5261
}
5362

5463
impl Default for Settings {
@@ -70,6 +79,9 @@ impl Default for Settings {
7079
launch_at_startup: false,
7180
confirm_folder_removal: true,
7281
sticky_toolbar: true,
82+
compact_list: false,
83+
show_time_badge: true,
84+
show_folder_column: false,
7385
}
7486
}
7587
}

‎src-tauri/tauri.conf.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Stack",
4-
"version": "1.0.12",
4+
"version": "1.0.13",
55
"identifier": "app.stack.desktop",
66
"build": {
77
"beforeDevCommand": "npm run dev",

‎src/components/asset/AssetColumnHeader.tsx‎

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
/**
22
* Sticky column header that mirrors AssetRow's layout. Per-type column sets
33
* (samples/midi keep waveform/time/key/bpm; presets show only Plugin; projects
4-
* show DAW + Date) are driven by the `viewType` prop.
4+
* show DAW + Date) are driven by the `viewType` prop. Right-click opens a menu
5+
* that toggles column visibility and row density (persisted in Settings).
56
*/
7+
import { useState } from 'react';
68
import { useSelectionStore } from '../../stores/selectionStore';
9+
import { useColumnPrefs } from '../../hooks/useColumnPrefs';
710
import { Checkbox } from '../ui/Checkbox';
11+
import { ContextMenu } from '../ui/ContextMenu';
812
import { COL } from './assetColumns';
913

1014
export type AssetViewType = 'sample' | 'midi' | 'preset' | 'project' | 'favorites';
@@ -18,14 +22,22 @@ export function AssetColumnHeader({
1822
showWaveform = true,
1923
showBpmBadge = true,
2024
showKeyBadge = true,
25+
showTimeBadge = true,
26+
showFolderColumn = false,
27+
compact = false,
2128
}: {
2229
viewType?: AssetViewType;
2330
pageAssetIds?: string[];
2431
showWaveform?: boolean;
2532
showBpmBadge?: boolean;
2633
showKeyBadge?: boolean;
34+
showTimeBadge?: boolean;
35+
showFolderColumn?: boolean;
36+
compact?: boolean;
2737
}) {
2838
const { selectedIds, setSelection, clearSelection } = useSelectionStore();
39+
const { settings, toggle } = useColumnPrefs();
40+
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
2941

3042
const selectedCount = pageAssetIds.filter((id) => selectedIds.has(id)).length;
3143
const allSelected = pageAssetIds.length > 0 && selectedCount === pageAssetIds.length;
@@ -39,10 +51,17 @@ export function AssetColumnHeader({
3951
}
4052
};
4153

54+
const mark = (on: boolean) => (on ? '\u2713 ' : '\u2007\u2007 ');
55+
4256
return (
4357
<div
4458
className="sticky top-0 z-10 flex shrink-0 items-center border-b border-gray-700 bg-gray-900"
4559
style={{ height: 32, paddingLeft: '12px', paddingRight: '12px' }}
60+
onContextMenu={(e) => {
61+
e.preventDefault();
62+
setMenu({ x: e.clientX, y: e.clientY });
63+
}}
64+
title="Right-click to choose columns"
4665
>
4766
{/* ── CHECKBOX: select-all ── */}
4867
<div
@@ -59,8 +78,8 @@ export function AssetColumnHeader({
5978

6079
{/* ── LEFT: Filename ── */}
6180
<div className="flex min-w-0 flex-[0.85] items-center" style={{ minWidth: '200px' }}>
62-
{/* Spacer for artwork (42) + play (32 on sm) + gap (8+12) */}
63-
<div style={{ width: 42 + 32 + 20 }} className="shrink-0" />
81+
{/* Spacer for artwork + play + gaps — must track AssetRow's geometry */}
82+
<div style={{ width: compact ? 28 + 24 + 20 : 42 + 32 + 20 }} className="shrink-0" />
6483
<span className={LABEL_CLS}>Filename</span>
6584
</div>
6685

@@ -75,9 +94,11 @@ export function AssetColumnHeader({
7594
</span>
7695
</div>
7796
)}
78-
<div className="shrink-0 text-right" style={{ minWidth: '40px' }}>
79-
<span className={MONO_CLS}>Time</span>
80-
</div>
97+
{showTimeBadge && (
98+
<div className="shrink-0 text-right" style={{ minWidth: '40px' }}>
99+
<span className={MONO_CLS}>Time</span>
100+
</div>
101+
)}
81102
{showKeyBadge && (
82103
<div className="shrink-0 text-center" style={{ minWidth: '50px' }}>
83104
<span className={MONO_CLS}>Key</span>
@@ -109,8 +130,32 @@ export function AssetColumnHeader({
109130
)}
110131
</div>
111132

133+
{/* ── FOLDER: optional path column ── */}
134+
{showFolderColumn && (
135+
<div className="hidden shrink-0 xl:block" style={{ width: 160 }}>
136+
<span className={LABEL_CLS}>Folder</span>
137+
</div>
138+
)}
139+
112140
{/* ── RIGHT: spacer for fav + more ── */}
113141
<div className="shrink-0" style={{ minWidth: '50px' }} />
142+
143+
{menu && settings && (
144+
<ContextMenu
145+
x={menu.x}
146+
y={menu.y}
147+
onClose={() => setMenu(null)}
148+
items={[
149+
{ label: `${mark(settings.showWaveform)}Waveform`, onSelect: () => toggle('showWaveform') },
150+
{ label: `${mark(settings.showTimeBadge)}Time`, onSelect: () => toggle('showTimeBadge') },
151+
{ label: `${mark(settings.showKeyBadge)}Key`, onSelect: () => toggle('showKeyBadge') },
152+
{ label: `${mark(settings.showBpmBadge)}BPM`, onSelect: () => toggle('showBpmBadge') },
153+
{ label: `${mark(settings.showFolderColumn)}Folder`, onSelect: () => toggle('showFolderColumn') },
154+
{ label: '—', onSelect: () => {} },
155+
{ label: `${mark(settings.compactList)}Compact rows`, onSelect: () => toggle('compactList') },
156+
]}
157+
/>
158+
)}
114159
</div>
115160
);
116161
}

‎src/components/asset/AssetGrid.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface AssetGridProps {
2929
}
3030

3131
const ROW_HEIGHT = 64;
32+
const COMPACT_ROW_HEIGHT = 40;
3233
const FOOTER_HEIGHT = 52;
3334

3435
export function AssetGrid({
@@ -60,6 +61,9 @@ export function AssetGrid({
6061
const showWaveform = settings?.showWaveform ?? true;
6162
const showBpmBadge = settings?.showBpmBadge ?? true;
6263
const showKeyBadge = settings?.showKeyBadge ?? true;
64+
const showTimeBadge = settings?.showTimeBadge ?? true;
65+
const showFolderColumn = settings?.showFolderColumn ?? false;
66+
const compactList = settings?.compactList ?? false;
6367

6468
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
6569
const selectedIndexRef = useRef<number | null>(null);
@@ -130,11 +134,16 @@ export function AssetGrid({
130134
const virtualizer = useVirtualizer({
131135
count: assets.length,
132136
getScrollElement: () => scrollContainerRef?.current ?? internalScrollRef.current,
133-
estimateSize: () => ROW_HEIGHT,
137+
estimateSize: () => (compactList ? COMPACT_ROW_HEIGHT : ROW_HEIGHT),
134138
overscan: 10,
135139
scrollMargin,
136140
});
137141

142+
// Cached row measurements go stale when density flips.
143+
useEffect(() => {
144+
virtualizer.measure();
145+
}, [compactList, virtualizer]);
146+
138147
// Set on pointer clicks so the scroll-into-view effect below knows the
139148
// selection came from the mouse (row already visible — don't nudge the list).
140149
const lastPointerSelectRef = useRef(0);
@@ -328,6 +337,9 @@ export function AssetGrid({
328337
showWaveform={showWaveform}
329338
showBpmBadge={showBpmBadge}
330339
showKeyBadge={showKeyBadge}
340+
showTimeBadge={showTimeBadge}
341+
showFolderColumn={showFolderColumn}
342+
compact={compactList}
331343
/>
332344
)}
333345

@@ -377,6 +389,9 @@ export function AssetGrid({
377389
showWaveform={showWaveform}
378390
showBpmBadge={showBpmBadge}
379391
showKeyBadge={showKeyBadge}
392+
showTimeBadge={showTimeBadge}
393+
showFolderColumn={showFolderColumn}
394+
compact={compactList}
380395
/>
381396
</div>
382397
);

‎src/components/asset/AssetRow.tsx‎

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ interface AssetRowProps {
5656
showWaveform?: boolean;
5757
showBpmBadge?: boolean;
5858
showKeyBadge?: boolean;
59+
showTimeBadge?: boolean;
60+
showFolderColumn?: boolean;
61+
compact?: boolean;
5962
}
6063

6164
export const AssetRow = memo(function AssetRow({
@@ -71,6 +74,9 @@ export const AssetRow = memo(function AssetRow({
7174
showWaveform = true,
7275
showBpmBadge = true,
7376
showKeyBadge = true,
77+
showTimeBadge = true,
78+
showFolderColumn = false,
79+
compact = false,
7480
}: AssetRowProps) {
7581
// Tier 1: ref-stable fields every row needs. Shallow-compared so a
7682
// currentTime tick doesn't invalidate this selector for any row.
@@ -238,6 +244,19 @@ export const AssetRow = memo(function AssetRow({
238244
}
239245
};
240246

247+
// "PackName/sub/folder" when the file lives under its pack, else the last
248+
// two path segments — enough to place the sample without the full path.
249+
const folderLabel = (() => {
250+
const norm = asset.path.replace(/\\/g, "/");
251+
const dir = norm.slice(0, norm.lastIndexOf("/"));
252+
if (packRoot) {
253+
const rootNorm = packRoot.replace(/\\/g, "/");
254+
const rootName = rootNorm.split("/").filter(Boolean).pop() ?? "";
255+
if (dir.startsWith(rootNorm)) return rootName + dir.slice(rootNorm.length);
256+
}
257+
return dir.split("/").filter(Boolean).slice(-2).join("/");
258+
})();
259+
241260
const categoryLabel =
242261
[asset.instrument, asset.subtype].filter(Boolean).join(" · ") || asset.type;
243262
const displayName =
@@ -350,7 +369,7 @@ export const AssetRow = memo(function AssetRow({
350369
? "bg-gray-800/70"
351370
: ""
352371
}`}
353-
style={{ height: 64, paddingLeft: "12px", paddingRight: "12px" }}
372+
style={{ height: compact ? 40 : 64, paddingLeft: "12px", paddingRight: "12px" }}
354373
draggable
355374
onDragStart={(e) => {
356375
e.preventDefault();
@@ -418,7 +437,7 @@ export const AssetRow = memo(function AssetRow({
418437
title={`Go to ${asset.packName ?? "pack folder"}`}
419438
aria-label="Go to pack folder"
420439
>
421-
<PackCover packRoot={packRoot} packName={asset.packName} size={42} />
440+
<PackCover packRoot={packRoot} packName={asset.packName} size={compact ? 28 : 42} />
422441
</button>
423442

424443
{/* Play / Open */}
@@ -488,9 +507,11 @@ export const AssetRow = memo(function AssetRow({
488507
>
489508
{displayName}
490509
</button>
491-
<div className="truncate text-xs capitalize leading-tight text-gray-500 mt-0.5">
492-
{categoryLabel}
493-
</div>
510+
{!compact && (
511+
<div className="truncate text-xs capitalize leading-tight text-gray-500 mt-0.5">
512+
{categoryLabel}
513+
</div>
514+
)}
494515
</div>
495516
</div>
496517

@@ -528,17 +549,17 @@ export const AssetRow = memo(function AssetRow({
528549
{asset.type === "sample" ? (
529550
<WaveformViewer
530551
assetId={asset.id}
531-
height={36}
552+
height={compact ? 20 : 36}
532553
progress={progress}
533554
onSeek={playFrom}
534555
/>
535556
) : asset.type === "midi" ? (
536557
<MidiViewer
537558
notes={(asset.meta as MidiMeta)?.pianoRoll ?? []}
538-
height={36}
559+
height={compact ? 20 : 36}
539560
/>
540561
) : (
541-
<div className="flex h-9 w-full items-center justify-center rounded-lg bg-gray-800">
562+
<div className={`flex w-full items-center justify-center rounded-lg bg-gray-800 ${compact ? "h-5" : "h-9"}`}>
542563
<Icon
543564
size={18}
544565
color="var(--color-text-muted)"
@@ -549,12 +570,14 @@ export const AssetRow = memo(function AssetRow({
549570
</div>
550571
)}
551572

552-
<div
553-
className="mono shrink-0 text-right text-xs sm:text-sm text-gray-500"
554-
style={{ minWidth: "40px" }}
555-
>
556-
{formatDuration(asset.durationMs)}
557-
</div>
573+
{showTimeBadge && (
574+
<div
575+
className="mono shrink-0 text-right text-xs sm:text-sm text-gray-500"
576+
style={{ minWidth: "40px" }}
577+
>
578+
{formatDuration(asset.durationMs)}
579+
</div>
580+
)}
558581

559582
{showKeyBadge && (
560583
<div
@@ -589,6 +612,21 @@ export const AssetRow = memo(function AssetRow({
589612
)}
590613
</div>
591614

615+
{/* ══ FOLDER: optional path column ══ */}
616+
{showFolderColumn && (
617+
<button
618+
onClick={(e) => {
619+
e.stopPropagation();
620+
goToFolder();
621+
}}
622+
className="mono hidden shrink-0 truncate text-left text-[11px] text-gray-500 transition-colors hover:text-stack-white xl:block"
623+
style={{ width: 160 }}
624+
title={asset.path}
625+
>
626+
{folderLabel}
627+
</button>
628+
)}
629+
592630
{/* ══ RIGHT: Favorite · More ══ */}
593631
<div
594632
className="flex shrink-0 items-center justify-end gap-0.5 sm:gap-1"

0 commit comments

Comments
 (0)