/** * MIDI Router View Component * Visual interface for creating and managing MIDI routes */ import { ArrowRight, Filter, GitBranch, Keyboard, Music, Plus, Settings, Shuffle, Zap } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { useMidiEngine } from "../../hooks/use-midi-engine" import type { MidiDestination, MidiRoute } from "../../services/midi/midi-router" interface RouteItemProps { route: MidiRoute onUpdate: (updates: Partial) => void onDelete: () => void devices: Array<{ id: string; name: string }> } function RouteItem({ route, onUpdate, onDelete, devices }: RouteItemProps) { const { t } = useTranslation() const getRouteIcon = () => { if (route.processors.some((p) => p.type === "split")) return if (route.processors.some((p) => p.type === "filter")) return if (route.processors.some((p) => p.type === "transform")) return return } const getSourceLabel = () => { const parts = [] if (route.sourceDevice) { const device = devices.find((d) => d.id === route.sourceDevice) parts.push(device?.name || route.sourceDevice) } else { parts.push(t("fairlightAudio.midi.router.source.anyDevice")) } if (route.sourceChannel) { parts.push(`${t("fairlightAudio.midi.router.source.channel")} ${route.sourceChannel}`) } if (route.sourceType?.length) { parts.push(`[${route.sourceType.join(", ")}]`) } return parts.join(" → ") } const getDestinationLabel = (dest: MidiDestination) => { switch (dest.type) { case "device": const device = devices.find((d) => d.id === dest.deviceId) return device?.name || dest.deviceId || "Unknown" case "channel": return `${t("fairlightAudio.midi.router.destination.channel")} ${dest.targetChannel}` case "virtual": return `${t("fairlightAudio.midi.router.destination.virtual")} ${dest.virtualId}` case "function": return t("fairlightAudio.midi.router.destination.functionCallback") default: return t("fairlightAudio.midi.router.destination.unknown") } } return (
onUpdate({ enabled })} aria-label={t("fairlightAudio.midi.router.route.enableRoute")} />
{getRouteIcon()} {route.name}
{route.enabled ? t("fairlightAudio.midi.router.route.active") : t("fairlightAudio.midi.router.route.inactive")} {t("fairlightAudio.midi.router.route.editRoute")} {t("fairlightAudio.midi.router.route.duplicate")} {t("fairlightAudio.midi.router.route.delete")}
{t("fairlightAudio.midi.router.source.from")} {getSourceLabel()}
{t("fairlightAudio.midi.router.destination.to")}
{route.destinations.map((dest, idx) => ( {getDestinationLabel(dest)} ))}
{route.processors.length > 0 && (
{t("fairlightAudio.midi.router.processors")}
{route.processors.map((proc, idx) => ( {proc.type} ))}
)}
) } export function MidiRouterView() { const { t } = useTranslation() const { engine, devices } = useMidiEngine() const [routes, setRoutes] = useState([]) const [selectedPreset, setSelectedPreset] = useState("") useEffect(() => { if (!engine?.router) return const updateRoutes = () => { if (engine.router) { setRoutes(engine.router.getRoutes()) } } // Initial load updateRoutes() // Listen for changes engine.router.on("routeCreated", updateRoutes) engine.router.on("routeUpdated", updateRoutes) engine.router.on("routeDeleted", updateRoutes) engine.router.on("routesReordered", updateRoutes) return () => { engine.router?.off("routeCreated", updateRoutes) engine.router?.off("routeUpdated", updateRoutes) engine.router?.off("routeDeleted", updateRoutes) engine.router?.off("routesReordered", updateRoutes) } }, [engine]) const handleCreatePreset = useCallback(() => { if (!engine?.router || !selectedPreset) return switch (selectedPreset) { case "keyboard-split": engine.router.createKeyboardSplitRoute( 60, // Middle C devices.output[0]?.id || "", devices.output[0]?.id || "", 1, 2, ) break case "channel-filter": engine.router.createChannelFilterRoute(1, devices.output[0]?.id || "") break case "cc-remap": engine.router.createCCRemapRoute( 1, // Mod wheel 11, // Expression devices.output[0]?.id, ) break default: console.warn("Unknown preset:", selectedPreset) break } setSelectedPreset("") }, [engine, devices, selectedPreset]) const handleUpdateRoute = useCallback( (routeId: string, updates: Partial) => { engine?.router?.updateRoute(routeId, updates) }, [engine], ) const handleDeleteRoute = useCallback( (routeId: string) => { engine?.router?.deleteRoute(routeId) }, [engine], ) if (!engine) { return ( {t("fairlightAudio.midi.router.title")}

{t("fairlightAudio.midi.router.engineNotInitialized")}

) } return ( {t("fairlightAudio.midi.router.title")}
{t("fairlightAudio.midi.router.tabs.routes")} {t("fairlightAudio.midi.router.tabs.matrixView")} {t("fairlightAudio.midi.router.tabs.monitor")} {routes.length === 0 ? (

{t("fairlightAudio.midi.router.noRoutes")}

{t("fairlightAudio.midi.router.createFirstRoute")}

) : (
{routes.map((route) => ( handleUpdateRoute(route.id, updates)} onDelete={() => handleDeleteRoute(route.id)} devices={[...devices.input, ...devices.output]} /> ))}
)}

{t("fairlightAudio.midi.router.matrixViewComingSoon")}

{t("fairlightAudio.midi.router.matrixDescription")}

{t("fairlightAudio.midi.router.monitorComingSoon")}

{t("fairlightAudio.midi.router.monitorDescription")}

) }