mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-16 08:10:40 +02:00
astra integration module
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { useEffect, useMemo, useState, type CSSProperties, type JSX } from 'react'
|
||||
import type { ScopeSettings } from '../../types/settings'
|
||||
import type { ResolvedAstraTheme } from '../../types/theme'
|
||||
import { useAstraStore } from '../stores/astraStore'
|
||||
import { formatAstraTime, getAstraPlaybackProgress } from '../utils/astra'
|
||||
|
||||
interface AstraScopeModuleProps {
|
||||
theme: ResolvedAstraTheme
|
||||
settings: ScopeSettings['astra']
|
||||
}
|
||||
|
||||
function hasVisibleFields(settings: ScopeSettings['astra']): boolean {
|
||||
return settings.showCoverArt
|
||||
|| settings.showTitle
|
||||
|| settings.showArtist
|
||||
|| settings.showProgress
|
||||
|| settings.showTime
|
||||
|| settings.showControls
|
||||
}
|
||||
|
||||
function getFallbackTitle(connectionState: ReturnType<typeof useAstraStore.getState>['integrationState']['connectionState']): string {
|
||||
switch (connectionState) {
|
||||
case 'disabled':
|
||||
return 'Astra is off'
|
||||
case 'connecting':
|
||||
return 'Connecting to Astra'
|
||||
case 'error':
|
||||
return 'Astra connection failed'
|
||||
case 'connected':
|
||||
return 'Nothing playing'
|
||||
}
|
||||
}
|
||||
|
||||
function getFallbackDetail(connectionState: ReturnType<typeof useAstraStore.getState>['integrationState']['connectionState']): string {
|
||||
switch (connectionState) {
|
||||
case 'disabled':
|
||||
return 'Open the Astra scope to connect.'
|
||||
case 'connecting':
|
||||
return 'Waiting for the Astra API.'
|
||||
case 'error':
|
||||
return 'Check the Astra base URL and token.'
|
||||
case 'connected':
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export default function AstraScopeModule({
|
||||
theme,
|
||||
settings,
|
||||
}: AstraScopeModuleProps): JSX.Element {
|
||||
const initialize = useAstraStore((s) => s.initialize)
|
||||
const setScopeActive = useAstraStore((s) => s.setScopeActive)
|
||||
const integrationState = useAstraStore((s) => s.integrationState)
|
||||
const isSendingControl = useAstraStore((s) => s.isSendingControl)
|
||||
const sendControl = useAstraStore((s) => s.sendControl)
|
||||
const [nowMs, setNowMs] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
void initialize()
|
||||
void setScopeActive(true)
|
||||
return () => {
|
||||
void setScopeActive(false)
|
||||
}
|
||||
}, [initialize, setScopeActive])
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationState.snapshot?.playbackState !== 'playing') {
|
||||
return
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
setNowMs(Date.now())
|
||||
}, 250)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [integrationState.snapshot?.playbackState])
|
||||
|
||||
const snapshot = integrationState.snapshot
|
||||
const currentTrack = snapshot?.currentTrack ?? null
|
||||
const liveProgress = useMemo(
|
||||
() => getAstraPlaybackProgress(snapshot, nowMs),
|
||||
[nowMs, snapshot],
|
||||
)
|
||||
const errorMessage = integrationState.lastError ?? integrationState.lastControlError
|
||||
const detailMessage = currentTrack?.artist
|
||||
?? (integrationState.connectionState === 'connected' ? null : getFallbackDetail(integrationState.connectionState))
|
||||
const style = {
|
||||
'--astra-accent': theme.accent,
|
||||
'--astra-bg': theme.background,
|
||||
'--astra-surface': theme.surface,
|
||||
'--astra-border': theme.border,
|
||||
'--astra-text': theme.text,
|
||||
'--astra-subtext': theme.subtext,
|
||||
'--astra-progress-track': theme.progressTrack,
|
||||
'--astra-progress-fill': theme.progressFill,
|
||||
'--astra-status-ok': theme.statusOk,
|
||||
'--astra-status-error': theme.statusError,
|
||||
} as CSSProperties
|
||||
|
||||
if (!hasVisibleFields(settings)) {
|
||||
return (
|
||||
<div className="astra-scope astra-scope--empty" style={style}>
|
||||
<div className="astra-scope__placeholder">
|
||||
All Astra elements are hidden.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const toggleCommand = snapshot?.playbackState === 'playing' ? 'pause' : 'play'
|
||||
const toggleLabel = snapshot?.playbackState === 'playing' ? 'Pause' : 'Play'
|
||||
const cardClassName = settings.showCoverArt
|
||||
? 'astra-scope__card'
|
||||
: 'astra-scope__card astra-scope__card--no-cover'
|
||||
|
||||
return (
|
||||
<div className="astra-scope" style={style}>
|
||||
<div className={cardClassName}>
|
||||
{settings.showCoverArt && (
|
||||
<div className="astra-scope__cover-shell">
|
||||
{currentTrack?.artworkDataUrl ? (
|
||||
<img
|
||||
className="astra-scope__cover"
|
||||
src={currentTrack.artworkDataUrl}
|
||||
alt={currentTrack.title}
|
||||
/>
|
||||
) : (
|
||||
<div className="astra-scope__cover astra-scope__cover--fallback" aria-hidden="true">
|
||||
<span>A</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="astra-scope__body">
|
||||
{(settings.showTitle || settings.showArtist) && (
|
||||
<div className="astra-scope__meta">
|
||||
{settings.showTitle && (
|
||||
<div className="astra-scope__title" title={currentTrack?.title ?? getFallbackTitle(integrationState.connectionState)}>
|
||||
{currentTrack?.title ?? getFallbackTitle(integrationState.connectionState)}
|
||||
</div>
|
||||
)}
|
||||
{settings.showArtist && detailMessage && (
|
||||
<div className="astra-scope__artist" title={detailMessage}>
|
||||
{detailMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(settings.showProgress || settings.showTime) && (
|
||||
<div className="astra-scope__transport">
|
||||
{settings.showProgress && (
|
||||
<div className="astra-scope__progress" aria-hidden="true">
|
||||
<div
|
||||
className="astra-scope__progress-fill"
|
||||
style={{ width: `${Math.max(0, Math.min(100, liveProgress.progress * 100))}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{settings.showTime && (
|
||||
<div className="astra-scope__time">
|
||||
<span>{formatAstraTime(liveProgress.currentTime)}</span>
|
||||
<span>/</span>
|
||||
<span>{liveProgress.duration > 0 ? formatAstraTime(liveProgress.duration) : '--:--'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{settings.showControls && (
|
||||
<div className="astra-scope__controls">
|
||||
<button
|
||||
type="button"
|
||||
className="astra-scope__control"
|
||||
disabled={!currentTrack || isSendingControl}
|
||||
onClick={() => {
|
||||
void sendControl('previous')
|
||||
}}
|
||||
aria-label="Previous track"
|
||||
title="Previous track"
|
||||
>
|
||||
⏮
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="astra-scope__control astra-scope__control--primary"
|
||||
disabled={!currentTrack || isSendingControl}
|
||||
onClick={() => {
|
||||
void sendControl(toggleCommand)
|
||||
}}
|
||||
aria-label={toggleLabel}
|
||||
title={toggleLabel}
|
||||
>
|
||||
{toggleLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="astra-scope__control"
|
||||
disabled={!currentTrack || isSendingControl}
|
||||
onClick={() => {
|
||||
void sendControl('next')
|
||||
}}
|
||||
aria-label="Next track"
|
||||
title="Next track"
|
||||
>
|
||||
⏭
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<div
|
||||
className="astra-scope__status is-error"
|
||||
>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX, type WheelEvent } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type JSX, type WheelEvent } from 'react'
|
||||
import { useAstraStore } from '../stores/astraStore'
|
||||
import { useAudioStore } from '../stores/audioStore'
|
||||
import { usePerformanceStore } from '../stores/performanceStore'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
@@ -7,6 +8,7 @@ import { getHorizontalWheelScrollResult } from '../utils/horizontalWheelScroll'
|
||||
import type { ScopeKind } from '../../types/scope'
|
||||
import { VISUALIZER_FRAME_TARGETS, type VisualizerFrameTarget } from '../../types/performance'
|
||||
import { SCOPE_KINDS } from '../../types/scope'
|
||||
import type { AstraIntegrationConfig } from '../../types/astra'
|
||||
import ThemedSelect from './ThemedSelect'
|
||||
|
||||
const SCOPE_LABELS: Record<ScopeKind, string> = {
|
||||
@@ -17,6 +19,7 @@ const SCOPE_LABELS: Record<ScopeKind, string> = {
|
||||
vumeter: 'VU Meter',
|
||||
lufsmeter: 'LUFS Meter',
|
||||
waveform: 'Waveform',
|
||||
astra: 'Astra',
|
||||
}
|
||||
|
||||
interface BottomBarProps {
|
||||
@@ -35,8 +38,11 @@ const FRAME_TARGET_LABELS: Record<VisualizerFrameTarget, string> = {
|
||||
|
||||
export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element {
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
const [astraBaseUrlInput, setAstraBaseUrlInput] = useState('')
|
||||
const [astraTokenInput, setAstraTokenInput] = useState('')
|
||||
|
||||
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
|
||||
const scopeOrder = useSettingsStore((s) => s.scopeOrder)
|
||||
const toggleScope = useSettingsStore((s) => s.toggleScope)
|
||||
const frameTarget = usePerformanceStore((s) => s.frameTarget)
|
||||
const dockedRenderFps = usePerformanceStore((s) => s.dockedRenderFps)
|
||||
@@ -53,6 +59,8 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
importThemeFromDialog,
|
||||
showThemesFolder,
|
||||
} = useThemeStore()
|
||||
const astraState = useAstraStore((s) => s.integrationState)
|
||||
const saveAstraConfig = useAstraStore((s) => s.saveConfig)
|
||||
|
||||
const {
|
||||
systemSources,
|
||||
@@ -79,6 +87,11 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
void refreshDevices()
|
||||
}, [refreshBackendSupport, refreshSystemSources, refreshDevices])
|
||||
|
||||
useEffect(() => {
|
||||
setAstraBaseUrlInput(astraState.config.baseUrl)
|
||||
setAstraTokenInput(astraState.config.token)
|
||||
}, [astraState.config.baseUrl, astraState.config.token])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!onHeightChange || !rootRef.current) return
|
||||
|
||||
@@ -168,6 +181,14 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
setThemeId(useThemeStore.getState().activeThemeId)
|
||||
}
|
||||
|
||||
const handleSaveAstraConfig = async (): Promise<void> => {
|
||||
const nextConfig: AstraIntegrationConfig = {
|
||||
baseUrl: astraBaseUrlInput,
|
||||
token: astraTokenInput,
|
||||
}
|
||||
await saveAstraConfig(nextConfig)
|
||||
}
|
||||
|
||||
const handleRailWheel = (event: WheelEvent<HTMLDivElement>): void => {
|
||||
const railElement = event.currentTarget
|
||||
const target = event.target
|
||||
@@ -190,6 +211,14 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const astraStatusLabel = astraState.connectionState === 'connected'
|
||||
? 'Connected'
|
||||
: astraState.connectionState === 'connecting'
|
||||
? 'Connecting'
|
||||
: astraState.connectionState === 'error'
|
||||
? 'Error'
|
||||
: 'Off'
|
||||
|
||||
return (
|
||||
<div className="bottom-bar" ref={rootRef}>
|
||||
<div className="bottom-bar__rail" aria-label="Global settings" onWheel={handleRailWheel}>
|
||||
@@ -199,7 +228,7 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
<div className="bottom-bar__section-body">
|
||||
<div className="bottom-bar__inline bottom-bar__inline--chips">
|
||||
{SCOPE_KINDS.map((kind) => {
|
||||
const active = !hiddenScopes.has(kind)
|
||||
const active = scopeOrder.includes(kind) && !hiddenScopes.has(kind)
|
||||
return (
|
||||
<button
|
||||
key={kind}
|
||||
@@ -297,6 +326,50 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
|
||||
<div className="bottom-bar__divider" />
|
||||
|
||||
<section className="bottom-bar__section bottom-bar__section--astra">
|
||||
<div className="bottom-bar__section-title">Astra</div>
|
||||
<div className="bottom-bar__section-body">
|
||||
<div className="bottom-bar__inline bottom-bar__inline--theme">
|
||||
<input
|
||||
className="bottom-bar__text-input bottom-bar__text-input--url"
|
||||
type="text"
|
||||
value={astraBaseUrlInput}
|
||||
placeholder="Astra Base URL"
|
||||
onChange={(event) => setAstraBaseUrlInput(event.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
className="bottom-bar__text-input bottom-bar__text-input--token"
|
||||
type="password"
|
||||
value={astraTokenInput}
|
||||
placeholder="Astra API Token"
|
||||
onChange={(event) => setAstraTokenInput(event.target.value)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="settings-chip"
|
||||
onClick={() => {
|
||||
void handleSaveAstraConfig()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
<div className={`settings-status-pill ${astraState.connectionState === 'disabled' ? '' : `is-${astraState.connectionState}`}`.trim()}>
|
||||
<span className="settings-status-pill__dot" />
|
||||
<span>{astraStatusLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{astraState.lastError ? (
|
||||
<div className="settings-error-text bottom-bar__error-text">{astraState.lastError}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="bottom-bar__divider" />
|
||||
|
||||
<section className="bottom-bar__section bottom-bar__section--source">
|
||||
<div className="bottom-bar__section-title">Audio Source</div>
|
||||
<div className="bottom-bar__section-body">
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ScopeKind } from '../../types/scope'
|
||||
import type { ScopeSettings } from '../../types/settings'
|
||||
import type {
|
||||
PrismResolvedTheme,
|
||||
ResolvedAstraTheme,
|
||||
ResolvedLUFSMeterTheme,
|
||||
ResolvedOscilloscopeTheme,
|
||||
ResolvedSpectrogramTheme,
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
} from '../../types/theme'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import AstraScopeModule from './AstraScopeModule'
|
||||
import { SpectrumAnalyzer, type SpectrumAnalyzerDataSource } from '../visualizers/SpectrumAnalyzer'
|
||||
import { Oscilloscope, type OscilloscopeDataSource } from '../visualizers/Oscilloscope'
|
||||
import { Vectorscope, type VectorscopeDataSource } from '../visualizers/Vectorscope'
|
||||
@@ -30,6 +32,7 @@ type ScopeModuleTheme =
|
||||
| ResolvedVUMeterTheme
|
||||
| ResolvedLUFSMeterTheme
|
||||
| ResolvedWaveformTheme
|
||||
| ResolvedAstraTheme
|
||||
|
||||
interface ScopeModuleProps {
|
||||
scopeKind: ScopeKind
|
||||
@@ -172,6 +175,8 @@ export function scopeSettingsToOptions(
|
||||
multiband: s.multiband,
|
||||
}
|
||||
}
|
||||
case 'astra':
|
||||
return {}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
@@ -222,6 +227,8 @@ function createVisualizer(
|
||||
...opts,
|
||||
...(dataSource ? { dataSource: dataSource as WaveformDataSource } : {}),
|
||||
})
|
||||
case 'astra':
|
||||
return null
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -244,6 +251,15 @@ export default function ScopeModule({
|
||||
const mySettings = settings ?? storeSettings
|
||||
const myTheme = theme ?? getScopeTheme(activeTheme, scopeKind)
|
||||
|
||||
if (scopeKind === 'astra') {
|
||||
return (
|
||||
<AstraScopeModule
|
||||
theme={myTheme as ResolvedAstraTheme}
|
||||
settings={mySettings as ScopeSettings['astra']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
@@ -10,17 +10,24 @@ import type {
|
||||
ScopePopoutSnapshot,
|
||||
ScopePopoutSyncStateMap,
|
||||
} from '../../types/popout'
|
||||
import { SCOPE_KINDS, SCOPE_LABELS, type ScopeKind } from '../../types/scope'
|
||||
import {
|
||||
AUDIO_SCOPE_KINDS,
|
||||
SCOPE_KINDS,
|
||||
SCOPE_LABELS,
|
||||
isAudioScopeKind,
|
||||
type AudioScopeKind,
|
||||
type ScopeKind,
|
||||
} from '../../types/scope'
|
||||
import type { ScopeSettings } from '../../types/settings'
|
||||
|
||||
function buildConsumerDemand(kind: ScopeKind): Record<ScopeKind, boolean> {
|
||||
return SCOPE_KINDS.reduce((acc, currentKind) => {
|
||||
function buildConsumerDemand(kind: AudioScopeKind): Record<AudioScopeKind, boolean> {
|
||||
return AUDIO_SCOPE_KINDS.reduce((acc, currentKind) => {
|
||||
acc[currentKind] = currentKind === kind
|
||||
return acc
|
||||
}, {} as Record<ScopeKind, boolean>)
|
||||
}, {} as Record<AudioScopeKind, boolean>)
|
||||
}
|
||||
|
||||
function flushScopeAudioBatch(kind: ScopeKind, scopeSettings: ScopeSettings): ScopePopoutAudioBatch {
|
||||
function flushScopeAudioBatch(kind: AudioScopeKind, scopeSettings: ScopeSettings): ScopePopoutAudioBatch {
|
||||
switch (kind) {
|
||||
case 'spectrum':
|
||||
return scopeSettings.spectrum.showSideLine
|
||||
@@ -111,6 +118,7 @@ export default function ScopePopoutBridge(): null {
|
||||
useEffect(() => {
|
||||
const sessionState = toPopoutSessionState(audioRouter.getSessionState())
|
||||
for (const kind of activePopoutKinds) {
|
||||
if (!isAudioScopeKind(kind)) continue
|
||||
window.electronAPI.sendScopePopoutSession(kind, sessionState)
|
||||
}
|
||||
}, [activePopoutKinds])
|
||||
@@ -138,7 +146,9 @@ export default function ScopePopoutBridge(): null {
|
||||
scopeTheme: useThemeStore.getState().activeTheme[kind],
|
||||
settings: useSettingsStore.getState().scopeSettings[kind],
|
||||
})
|
||||
window.electronAPI.sendScopePopoutSession(kind, toPopoutSessionState(audioRouter.getSessionState()))
|
||||
if (isAudioScopeKind(kind)) {
|
||||
window.electronAPI.sendScopePopoutSession(kind, toPopoutSessionState(audioRouter.getSessionState()))
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
@@ -150,7 +160,7 @@ export default function ScopePopoutBridge(): null {
|
||||
}, [popInScope, updatePopoutBounds, updateScopeSettings])
|
||||
|
||||
useEffect(() => {
|
||||
for (const kind of SCOPE_KINDS) {
|
||||
for (const kind of AUDIO_SCOPE_KINDS) {
|
||||
const consumerId = `popout:${kind}`
|
||||
if (activePopoutKinds.includes(kind)) {
|
||||
audioRouter.setVisualizerConsumerDemand(consumerId, buildConsumerDemand(kind))
|
||||
@@ -160,7 +170,7 @@ export default function ScopePopoutBridge(): null {
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const kind of SCOPE_KINDS) {
|
||||
for (const kind of AUDIO_SCOPE_KINDS) {
|
||||
audioRouter.clearVisualizerConsumerDemand(`popout:${kind}`)
|
||||
}
|
||||
}
|
||||
@@ -175,6 +185,7 @@ export default function ScopePopoutBridge(): null {
|
||||
}
|
||||
|
||||
for (const kind of activePopoutKindsRef.current) {
|
||||
if (!isAudioScopeKind(kind)) continue
|
||||
const batch = flushScopeAudioBatch(kind, useSettingsStore.getState().scopeSettings)
|
||||
if (batch.length > 0) {
|
||||
window.electronAPI.sendScopePopoutAudio(kind, batch)
|
||||
@@ -216,6 +227,7 @@ export default function ScopePopoutBridge(): null {
|
||||
return audioRouter.subscribeToSessionChanges((state) => {
|
||||
const nextSessionState = toPopoutSessionState(state)
|
||||
for (const kind of activePopoutKindsRef.current) {
|
||||
if (!isAudioScopeKind(kind)) continue
|
||||
window.electronAPI.sendScopePopoutSession(kind, nextSessionState)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -19,6 +19,17 @@ function vectorscopeModeLabel(mode: ScopeSettings['vectorscope']['mode']): strin
|
||||
}
|
||||
}
|
||||
|
||||
function astraVisibleLabels(settings: ScopeSettings['astra']): string[] {
|
||||
const labels: string[] = []
|
||||
if (settings.showCoverArt) labels.push('Cover')
|
||||
if (settings.showTitle) labels.push('Title')
|
||||
if (settings.showArtist) labels.push('Artist')
|
||||
if (settings.showProgress) labels.push('Bar')
|
||||
if (settings.showTime) labels.push('Time')
|
||||
if (settings.showControls) labels.push('Controls')
|
||||
return labels
|
||||
}
|
||||
|
||||
export function scopeSummary(kind: ScopeKind, settings: ScopeSettings[ScopeKind]): string {
|
||||
switch (kind) {
|
||||
case 'spectrum': {
|
||||
@@ -58,6 +69,10 @@ export function scopeSummary(kind: ScopeKind, settings: ScopeSettings[ScopeKind]
|
||||
}
|
||||
return summary.join(' · ')
|
||||
}
|
||||
case 'astra': {
|
||||
const visible = astraVisibleLabels(settings as ScopeSettings['astra'])
|
||||
return visible.length > 0 ? visible.join(' · ') : 'Hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +514,44 @@ export default function ScopeSettingsSection({
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
|
||||
{kind === 'astra' && (() => {
|
||||
const current = settings as ScopeSettings['astra']
|
||||
return (
|
||||
<ToggleGroup label="Visible Elements">
|
||||
<ToggleChip
|
||||
label="Cover"
|
||||
active={current.showCoverArt}
|
||||
onClick={() => onUpdate('astra', { showCoverArt: !current.showCoverArt })}
|
||||
/>
|
||||
<ToggleChip
|
||||
label="Title"
|
||||
active={current.showTitle}
|
||||
onClick={() => onUpdate('astra', { showTitle: !current.showTitle })}
|
||||
/>
|
||||
<ToggleChip
|
||||
label="Artist"
|
||||
active={current.showArtist}
|
||||
onClick={() => onUpdate('astra', { showArtist: !current.showArtist })}
|
||||
/>
|
||||
<ToggleChip
|
||||
label="Bar"
|
||||
active={current.showProgress}
|
||||
onClick={() => onUpdate('astra', { showProgress: !current.showProgress })}
|
||||
/>
|
||||
<ToggleChip
|
||||
label="Time"
|
||||
active={current.showTime}
|
||||
onClick={() => onUpdate('astra', { showTime: !current.showTime })}
|
||||
/>
|
||||
<ToggleChip
|
||||
label="Controls"
|
||||
active={current.showControls}
|
||||
onClick={() => onUpdate('astra', { showControls: !current.showControls })}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type JSX } from 'react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { SCOPE_LABELS, type ScopeKind } from '../../types/scope'
|
||||
import { SCOPE_LABELS, isAudioScopeKind, type ScopeKind } from '../../types/scope'
|
||||
import type { WindowBounds } from '../../types/popout'
|
||||
import ScopeModule from './ScopeModule'
|
||||
import { buildAnalyzerGridTemplateColumns } from '../analyzerLayout'
|
||||
@@ -68,15 +68,15 @@ export default function Strip(): JSX.Element {
|
||||
}, [dockedScopes])
|
||||
|
||||
useEffect(() => {
|
||||
const visibleScopeSet = new Set(dockedScopes)
|
||||
const visibleAudioScopeSet = new Set(dockedScopes.filter(isAudioScopeKind))
|
||||
audioRouter.setVisualizerConsumerDemand('docked-strip', {
|
||||
spectrum: visibleScopeSet.has('spectrum'),
|
||||
oscilloscope: visibleScopeSet.has('oscilloscope'),
|
||||
vectorscope: visibleScopeSet.has('vectorscope'),
|
||||
spectrogram: visibleScopeSet.has('spectrogram'),
|
||||
vumeter: visibleScopeSet.has('vumeter'),
|
||||
lufsmeter: visibleScopeSet.has('lufsmeter'),
|
||||
waveform: visibleScopeSet.has('waveform'),
|
||||
spectrum: visibleAudioScopeSet.has('spectrum'),
|
||||
oscilloscope: visibleAudioScopeSet.has('oscilloscope'),
|
||||
vectorscope: visibleAudioScopeSet.has('vectorscope'),
|
||||
spectrogram: visibleAudioScopeSet.has('spectrogram'),
|
||||
vumeter: visibleAudioScopeSet.has('vumeter'),
|
||||
lufsmeter: visibleAudioScopeSet.has('lufsmeter'),
|
||||
waveform: visibleAudioScopeSet.has('waveform'),
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user