mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-19 03:54:23 +02:00
reorder the scopes
This commit is contained in:
@@ -25,3 +25,6 @@ Thumbs.db
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
# notes
|
||||||
|
untitled-1
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function Strip(): JSX.Element {
|
|||||||
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
|
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
|
||||||
const scopePopouts = useSettingsStore((s) => s.scopePopouts)
|
const scopePopouts = useSettingsStore((s) => s.scopePopouts)
|
||||||
const widthWeights = useSettingsStore((s) => s.widthWeights)
|
const widthWeights = useSettingsStore((s) => s.widthWeights)
|
||||||
|
const moveDockedScope = useSettingsStore((s) => s.moveDockedScope)
|
||||||
const setScopeWidthWeight = useSettingsStore((s) => s.setScopeWidthWeight)
|
const setScopeWidthWeight = useSettingsStore((s) => s.setScopeWidthWeight)
|
||||||
const popOutScope = useSettingsStore((s) => s.popOutScope)
|
const popOutScope = useSettingsStore((s) => s.popOutScope)
|
||||||
const accent = useThemeStore((s) => s.accent)
|
const accent = useThemeStore((s) => s.accent)
|
||||||
@@ -192,7 +193,7 @@ export default function Strip(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div ref={stripRef} className="scope-strip">
|
<div ref={stripRef} className="scope-strip">
|
||||||
<div ref={gridRef} className="scope-strip__grid" style={gridStyle}>
|
<div ref={gridRef} className="scope-strip__grid" style={gridStyle}>
|
||||||
{dockedScopes.map((kind) => (
|
{dockedScopes.map((kind, index) => (
|
||||||
<div
|
<div
|
||||||
key={kind}
|
key={kind}
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
@@ -200,6 +201,34 @@ export default function Strip(): JSX.Element {
|
|||||||
}}
|
}}
|
||||||
className="scope-strip__cell"
|
className="scope-strip__cell"
|
||||||
>
|
>
|
||||||
|
{dockedScopes.length > 1 && (
|
||||||
|
<div className="scope-strip__reorder-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scope-strip__reorder-button"
|
||||||
|
onClick={() => moveDockedScope(kind, 'left')}
|
||||||
|
aria-label={`Move ${SCOPE_LABELS[kind]} left`}
|
||||||
|
title={`Move ${SCOPE_LABELS[kind]} left`}
|
||||||
|
disabled={index === 0}
|
||||||
|
>
|
||||||
|
<span className="scope-strip__reorder-icon" aria-hidden="true">
|
||||||
|
←
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scope-strip__reorder-button"
|
||||||
|
onClick={() => moveDockedScope(kind, 'right')}
|
||||||
|
aria-label={`Move ${SCOPE_LABELS[kind]} right`}
|
||||||
|
title={`Move ${SCOPE_LABELS[kind]} right`}
|
||||||
|
disabled={index === dockedScopes.length - 1}
|
||||||
|
>
|
||||||
|
<span className="scope-strip__reorder-icon" aria-hidden="true">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="scope-strip__popout-button"
|
className="scope-strip__popout-button"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ interface SettingsState {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleScope: (kind: ScopeKind) => void
|
toggleScope: (kind: ScopeKind) => void
|
||||||
moveScope: (kind: ScopeKind, direction: 'left' | 'right') => void
|
moveDockedScope: (kind: ScopeKind, direction: 'left' | 'right') => void
|
||||||
setScopeWidthWeight: (kind: ScopeKind, weight: number) => void
|
setScopeWidthWeight: (kind: ScopeKind, weight: number) => void
|
||||||
updateScopeSettings: <K extends ScopeKind>(kind: K, settings: Partial<ScopeSettings[K]>) => void
|
updateScopeSettings: <K extends ScopeKind>(kind: K, settings: Partial<ScopeSettings[K]>) => void
|
||||||
popOutScope: (kind: ScopeKind, bounds?: WindowBounds) => void
|
popOutScope: (kind: ScopeKind, bounds?: WindowBounds) => void
|
||||||
@@ -199,6 +199,49 @@ function normalizeScopePopouts(raw: unknown): ScopePopoutStateMap {
|
|||||||
return defaults
|
return defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDockedScope(
|
||||||
|
kind: ScopeKind,
|
||||||
|
hiddenScopes: ReadonlySet<ScopeKind>,
|
||||||
|
scopePopouts: ScopePopoutStateMap,
|
||||||
|
): boolean {
|
||||||
|
return !hiddenScopes.has(kind) && !scopePopouts[kind]?.poppedOut
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveDockedScopeOrder(
|
||||||
|
scopeOrder: ScopeKind[],
|
||||||
|
hiddenScopes: ReadonlySet<ScopeKind>,
|
||||||
|
scopePopouts: ScopePopoutStateMap,
|
||||||
|
kind: ScopeKind,
|
||||||
|
direction: 'left' | 'right',
|
||||||
|
): ScopeKind[] {
|
||||||
|
const dockedScopes = scopeOrder.filter((scope) => isDockedScope(scope, hiddenScopes, scopePopouts))
|
||||||
|
const index = dockedScopes.indexOf(kind)
|
||||||
|
if (index === -1) return scopeOrder
|
||||||
|
|
||||||
|
const targetIndex = direction === 'left' ? index - 1 : index + 1
|
||||||
|
if (targetIndex < 0 || targetIndex >= dockedScopes.length) return scopeOrder
|
||||||
|
|
||||||
|
const reorderedDockedScopes = [...dockedScopes]
|
||||||
|
;[reorderedDockedScopes[index], reorderedDockedScopes[targetIndex]] = [
|
||||||
|
reorderedDockedScopes[targetIndex],
|
||||||
|
reorderedDockedScopes[index],
|
||||||
|
]
|
||||||
|
|
||||||
|
let reorderedIndex = 0
|
||||||
|
let didChange = false
|
||||||
|
const mergedOrder = scopeOrder.map((scope) => {
|
||||||
|
if (!isDockedScope(scope, hiddenScopes, scopePopouts)) return scope
|
||||||
|
const nextScope = reorderedDockedScopes[reorderedIndex] ?? scope
|
||||||
|
reorderedIndex += 1
|
||||||
|
if (nextScope !== scope) {
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
return nextScope
|
||||||
|
})
|
||||||
|
|
||||||
|
return didChange ? mergedOrder : scopeOrder
|
||||||
|
}
|
||||||
|
|
||||||
function cloneScopeSettings(settings: ScopeSettings): ScopeSettings {
|
function cloneScopeSettings(settings: ScopeSettings): ScopeSettings {
|
||||||
return JSON.parse(JSON.stringify(settings)) as ScopeSettings
|
return JSON.parse(JSON.stringify(settings)) as ScopeSettings
|
||||||
}
|
}
|
||||||
@@ -262,15 +305,17 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
moveScope: (kind: ScopeKind, direction: 'left' | 'right') => {
|
moveDockedScope: (kind: ScopeKind, direction: 'left' | 'right') => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const order = [...state.scopeOrder]
|
const nextOrder = moveDockedScopeOrder(
|
||||||
const idx = order.indexOf(kind)
|
state.scopeOrder,
|
||||||
if (idx === -1) return state
|
state.hiddenScopes,
|
||||||
const swap = direction === 'left' ? idx - 1 : idx + 1
|
state.scopePopouts,
|
||||||
if (swap < 0 || swap >= order.length) return state
|
kind,
|
||||||
;[order[idx], order[swap]] = [order[swap], order[idx]]
|
direction,
|
||||||
const newState = { ...state, scopeOrder: order }
|
)
|
||||||
|
if (nextOrder === state.scopeOrder) return state
|
||||||
|
const newState = { ...state, scopeOrder: nextOrder }
|
||||||
saveToStorage(newState as SettingsState)
|
saveToStorage(newState as SettingsState)
|
||||||
return newState
|
return newState
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -342,13 +342,24 @@ select {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-strip__popout-button {
|
.scope-strip__reorder-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
left: 8px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-strip__reorder-button,
|
||||||
|
.scope-strip__popout-button {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
background: rgba(8, 12, 18, 0.74);
|
background: rgba(8, 12, 18, 0.74);
|
||||||
@@ -356,24 +367,57 @@ select {
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
transition: opacity 120ms ease, transform 120ms ease, border-color 120ms ease, color 120ms ease;
|
transition: opacity 120ms ease, transform 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||||
backdrop-filter: blur(14px) saturate(1.04);
|
backdrop-filter: blur(14px) saturate(1.04);
|
||||||
-webkit-backdrop-filter: blur(14px) saturate(1.04);
|
-webkit-backdrop-filter: blur(14px) saturate(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scope-strip__reorder-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-strip__popout-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 3;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-strip__cell:hover .scope-strip__reorder-controls,
|
||||||
|
.scope-strip__cell:focus-within .scope-strip__reorder-controls,
|
||||||
.scope-strip__cell:hover .scope-strip__popout-button,
|
.scope-strip__cell:hover .scope-strip__popout-button,
|
||||||
|
.scope-strip__cell:focus-within .scope-strip__popout-button,
|
||||||
.scope-strip__popout-button:focus-visible {
|
.scope-strip__popout-button:focus-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-strip__popout-button:hover {
|
.scope-strip__reorder-button:hover:not(:disabled),
|
||||||
|
.scope-strip__reorder-button:focus-visible,
|
||||||
|
.scope-strip__popout-button:hover,
|
||||||
|
.scope-strip__popout-button:focus-visible {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: rgba(var(--accent-rgb), 0.28);
|
border-color: rgba(var(--accent-rgb), 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scope-strip__reorder-button:focus-visible,
|
||||||
|
.scope-strip__popout-button:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-strip__reorder-button:disabled {
|
||||||
|
color: rgba(255, 255, 255, 0.32);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-strip__reorder-icon,
|
||||||
.scope-strip__popout-icon {
|
.scope-strip__popout-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import {
|
|||||||
parseColorToRgb,
|
parseColorToRgb,
|
||||||
resolveColorToRgb,
|
resolveColorToRgb,
|
||||||
} from '../src/renderer/utils/color'
|
} from '../src/renderer/utils/color'
|
||||||
|
import { moveDockedScopeOrder } from '../src/renderer/stores/settingsStore'
|
||||||
|
import { SCOPE_KINDS, type ScopeKind } from '../src/types/scope'
|
||||||
|
import type { ScopePopoutStateMap } from '../src/types/popout'
|
||||||
import {
|
import {
|
||||||
VUMeterBallistics,
|
VUMeterBallistics,
|
||||||
VU_INTEGRATION_WINDOW_MS,
|
VU_INTEGRATION_WINDOW_MS,
|
||||||
@@ -90,6 +93,14 @@ function createProgramSamples(length: number): { left: Float32Array; right: Floa
|
|||||||
return { left, right }
|
return { left, right }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createScopePopouts(poppedOutScopes: ScopeKind[] = []): ScopePopoutStateMap {
|
||||||
|
const poppedOutSet = new Set(poppedOutScopes)
|
||||||
|
return SCOPE_KINDS.reduce((acc, kind) => {
|
||||||
|
acc[kind] = { poppedOut: poppedOutSet.has(kind) }
|
||||||
|
return acc
|
||||||
|
}, {} as ScopePopoutStateMap)
|
||||||
|
}
|
||||||
|
|
||||||
test('parseColorToRgb handles hex, rgb, rgba, and percentage formats', () => {
|
test('parseColorToRgb handles hex, rgb, rgba, and percentage formats', () => {
|
||||||
assert.deepEqual(parseColorToRgb('#38bdf8'), { r: 56, g: 189, b: 248 })
|
assert.deepEqual(parseColorToRgb('#38bdf8'), { r: 56, g: 189, b: 248 })
|
||||||
assert.deepEqual(parseColorToRgb('#3bf'), { r: 51, g: 187, b: 255 })
|
assert.deepEqual(parseColorToRgb('#3bf'), { r: 51, g: 187, b: 255 })
|
||||||
@@ -190,6 +201,79 @@ test('VisualizerFrameLoop invalidate and stop manage subscriptions correctly', (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('moveDockedScopeOrder swaps a middle docked scope with its adjacent docked neighbor', () => {
|
||||||
|
const nextOrder = moveDockedScopeOrder(
|
||||||
|
[...SCOPE_KINDS],
|
||||||
|
new Set<ScopeKind>(),
|
||||||
|
createScopePopouts(),
|
||||||
|
'vectorscope',
|
||||||
|
'left',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(nextOrder, [
|
||||||
|
'spectrum',
|
||||||
|
'vectorscope',
|
||||||
|
'oscilloscope',
|
||||||
|
'spectrogram',
|
||||||
|
'vumeter',
|
||||||
|
'lufsmeter',
|
||||||
|
'waveform',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('moveDockedScopeOrder is a no-op at the docked boundaries', () => {
|
||||||
|
const initialOrder = [...SCOPE_KINDS]
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
moveDockedScopeOrder(initialOrder, new Set<ScopeKind>(), createScopePopouts(), 'spectrum', 'left'),
|
||||||
|
initialOrder,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
moveDockedScopeOrder(initialOrder, new Set<ScopeKind>(), createScopePopouts(), 'waveform', 'right'),
|
||||||
|
initialOrder,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('moveDockedScopeOrder preserves hidden scope positions in the full order', () => {
|
||||||
|
const nextOrder = moveDockedScopeOrder(
|
||||||
|
[...SCOPE_KINDS],
|
||||||
|
new Set<ScopeKind>(['oscilloscope']),
|
||||||
|
createScopePopouts(),
|
||||||
|
'vectorscope',
|
||||||
|
'left',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(nextOrder, [
|
||||||
|
'vectorscope',
|
||||||
|
'oscilloscope',
|
||||||
|
'spectrum',
|
||||||
|
'spectrogram',
|
||||||
|
'vumeter',
|
||||||
|
'lufsmeter',
|
||||||
|
'waveform',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('moveDockedScopeOrder preserves popped-out scope positions in the full order', () => {
|
||||||
|
const nextOrder = moveDockedScopeOrder(
|
||||||
|
[...SCOPE_KINDS],
|
||||||
|
new Set<ScopeKind>(),
|
||||||
|
createScopePopouts(['oscilloscope']),
|
||||||
|
'spectrogram',
|
||||||
|
'left',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(nextOrder, [
|
||||||
|
'spectrum',
|
||||||
|
'oscilloscope',
|
||||||
|
'spectrogram',
|
||||||
|
'vectorscope',
|
||||||
|
'vumeter',
|
||||||
|
'lufsmeter',
|
||||||
|
'waveform',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
test('VUMeterBallistics holds RMS and correlation steady when an active frame receives no chunks', () => {
|
test('VUMeterBallistics holds RMS and correlation steady when an active frame receives no chunks', () => {
|
||||||
const sampleRate = 48000
|
const sampleRate = 48000
|
||||||
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
||||||
|
|||||||
Reference in New Issue
Block a user