Mobile version is taking shape
This commit is contained in:
97
src/App.tsx
97
src/App.tsx
@ -36,6 +36,7 @@ function App() {
|
|||||||
const [customTile, setCustomTile] = useState<TileState>(() => createTileStateFromCurrent('t*(8&t>>9)'))
|
const [customTile, setCustomTile] = useState<TileState>(() => createTileStateFromCurrent('t*(8&t>>9)'))
|
||||||
const [showWarning, setShowWarning] = useState(true)
|
const [showWarning, setShowWarning] = useState(true)
|
||||||
const [showHelp, setShowHelp] = useState(false)
|
const [showHelp, setShowHelp] = useState(false)
|
||||||
|
const [mobileHeaderTab, setMobileHeaderTab] = useState<'global' | 'options' | 'modulate'>('global')
|
||||||
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
||||||
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
|
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
|
||||||
const switchTimerRef = useRef<number | null>(null)
|
const switchTimerRef = useRef<number | null>(null)
|
||||||
@ -542,44 +543,110 @@ function App() {
|
|||||||
{showWarning && <AudioContextWarning onDismiss={handleDismissWarning} />}
|
{showWarning && <AudioContextWarning onDismiss={handleDismissWarning} />}
|
||||||
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
|
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
|
||||||
<header className="bg-black border-b-2 border-white px-2 lg:px-6 py-2 lg:py-3">
|
<header className="bg-black border-b-2 border-white px-2 lg:px-6 py-2 lg:py-3">
|
||||||
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-2 lg:gap-6">
|
{/* Mobile header */}
|
||||||
<div className="flex items-center justify-between w-full lg:w-auto gap-2">
|
<div className="lg:hidden">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1
|
<h1
|
||||||
onClick={() => setShowHelp(true)}
|
onClick={() => setShowHelp(true)}
|
||||||
className="font-mono text-[10px] md:text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
|
className="font-mono text-[10px] tracking-[0.3em] text-white cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
>
|
>
|
||||||
BRUITISTE
|
BRUITISTE
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-2 lg:hidden">
|
<div className="flex gap-1 border-2 border-white">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileHeaderTab('global')}
|
||||||
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
|
mobileHeaderTab === 'global'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
GLOBAL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileHeaderTab('options')}
|
||||||
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
|
mobileHeaderTab === 'options'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
OPTIONS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileHeaderTab('modulate')}
|
||||||
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
|
mobileHeaderTab === 'modulate'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
MODULATE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mobileHeaderTab === 'global' && (
|
||||||
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={!playing}
|
disabled={!playing}
|
||||||
className="px-2 py-1 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
className="flex-1 px-2 py-2 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Square size={12} strokeWidth={2} fill="currentColor" />
|
<Square size={12} strokeWidth={2} fill="currentColor" className="mx-auto" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRandom}
|
onClick={handleRandom}
|
||||||
className="px-2 py-1 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
className="flex-1 px-2 py-2 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
||||||
>
|
>
|
||||||
<Dices size={12} strokeWidth={2} />
|
<Dices size={12} strokeWidth={2} className="mx-auto" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRandomizeAllParams}
|
onClick={handleRandomizeAllParams}
|
||||||
className="px-2 py-1 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
className="flex-1 px-2 py-2 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
|
||||||
>
|
>
|
||||||
<Sparkles size={12} strokeWidth={2} />
|
<Sparkles size={12} strokeWidth={2} className="mx-auto" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadAll}
|
onClick={handleDownloadAll}
|
||||||
disabled={downloading}
|
disabled={downloading}
|
||||||
className="px-2 py-1 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
className="flex-1 px-2 py-2 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Archive size={12} strokeWidth={2} />
|
<Archive size={12} strokeWidth={2} className="mx-auto" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="w-full lg:flex-1">
|
|
||||||
|
{mobileHeaderTab === 'options' && (
|
||||||
|
<EngineControls
|
||||||
|
values={engineValues}
|
||||||
|
onChange={handleEngineChange}
|
||||||
|
onMapClick={handleParameterMapClick}
|
||||||
|
getMappedLFOs={getMappedLFOs}
|
||||||
|
showOnlySliders
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mobileHeaderTab === 'modulate' && (
|
||||||
|
<EngineControls
|
||||||
|
values={engineValues}
|
||||||
|
onChange={handleEngineChange}
|
||||||
|
onMapClick={handleParameterMapClick}
|
||||||
|
getMappedLFOs={getMappedLFOs}
|
||||||
|
showOnlyKnobs
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop header */}
|
||||||
|
<div className="hidden lg:flex items-center gap-6">
|
||||||
|
<h1
|
||||||
|
onClick={() => setShowHelp(true)}
|
||||||
|
className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
|
>
|
||||||
|
BRUITISTE
|
||||||
|
</h1>
|
||||||
|
<div className="flex-1">
|
||||||
<EngineControls
|
<EngineControls
|
||||||
values={engineValues}
|
values={engineValues}
|
||||||
onChange={handleEngineChange}
|
onChange={handleEngineChange}
|
||||||
@ -587,7 +654,7 @@ function App() {
|
|||||||
getMappedLFOs={getMappedLFOs}
|
getMappedLFOs={getMappedLFOs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden lg:flex gap-2 lg:gap-4 flex-shrink-0">
|
<div className="flex gap-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={!playing}
|
disabled={!playing}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ interface EffectsBarProps {
|
|||||||
export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: EffectsBarProps) {
|
export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: EffectsBarProps) {
|
||||||
const [expandedEffect, setExpandedEffect] = useState<string | null>(null)
|
const [expandedEffect, setExpandedEffect] = useState<string | null>(null)
|
||||||
const [activeTab, setActiveTab] = useState<string>(EFFECTS[0].id)
|
const [activeTab, setActiveTab] = useState<string>(EFFECTS[0].id)
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||||
const randomizeEffect = (effect: typeof EFFECTS[number]) => {
|
const randomizeEffect = (effect: typeof EFFECTS[number]) => {
|
||||||
effect.parameters.forEach(param => {
|
effect.parameters.forEach(param => {
|
||||||
if (param.id.endsWith('Enable')) return
|
if (param.id.endsWith('Enable')) return
|
||||||
|
|||||||
@ -8,11 +8,13 @@ interface EngineControlsProps {
|
|||||||
onChange: (parameterId: string, value: number) => void
|
onChange: (parameterId: string, value: number) => void
|
||||||
onMapClick?: (paramId: string, lfoIndex: number) => void
|
onMapClick?: (paramId: string, lfoIndex: number) => void
|
||||||
getMappedLFOs?: (paramId: string) => number[]
|
getMappedLFOs?: (paramId: string) => number[]
|
||||||
|
showOnlySliders?: boolean
|
||||||
|
showOnlyKnobs?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const KNOB_PARAMS = ['masterVolume', 'pitch', 'a', 'b', 'c', 'd']
|
const KNOB_PARAMS = ['masterVolume', 'pitch', 'a', 'b', 'c', 'd']
|
||||||
|
|
||||||
export function EngineControls({ values, onChange, onMapClick, getMappedLFOs }: EngineControlsProps) {
|
export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, showOnlySliders, showOnlyKnobs }: EngineControlsProps) {
|
||||||
const formatValue = (id: string, value: number): string => {
|
const formatValue = (id: string, value: number): string => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'sampleRate':
|
case 'sampleRate':
|
||||||
@ -33,6 +35,9 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs }:
|
|||||||
{ENGINE_CONTROLS[0].parameters.map(param => {
|
{ENGINE_CONTROLS[0].parameters.map(param => {
|
||||||
const useKnob = KNOB_PARAMS.includes(param.id)
|
const useKnob = KNOB_PARAMS.includes(param.id)
|
||||||
|
|
||||||
|
if (showOnlySliders && useKnob) return null
|
||||||
|
if (showOnlyKnobs && !useKnob) return null
|
||||||
|
|
||||||
if (useKnob) {
|
if (useKnob) {
|
||||||
return (
|
return (
|
||||||
<Knob
|
<Knob
|
||||||
|
|||||||
Reference in New Issue
Block a user