From e8bb9c659106c52fed653114272d763882f7e1a0 Mon Sep 17 00:00:00 2001 From: user_1781515504933 Date: Wed, 30 Sep 2026 14:15:33 +0700 Subject: [PATCH 1/9] feat: add Noir S1 configurator workspace --- src/app/OverviewPage.tsx | 54 +++++++++++- src/app/cards/AdvancedCards.tsx | 20 ++++- src/app/cards/DpiCard.tsx | 12 ++- src/app/cards/PerformanceCards.tsx | 1 + src/app/cards/availability.test.ts | 36 ++++++++ src/app/cards/availability.ts | 10 +-- src/control.css | 4 + src/device/controller.ts | 132 +++++++++++++++++++++++++++-- src/device/m2nex-profiles.test.ts | 10 +++ src/device/m2nex-profiles.ts | 56 +++++++++--- 10 files changed, 302 insertions(+), 33 deletions(-) diff --git a/src/app/OverviewPage.tsx b/src/app/OverviewPage.tsx index 326df2f6..08e33f02 100644 --- a/src/app/OverviewPage.tsx +++ b/src/app/OverviewPage.tsx @@ -94,10 +94,23 @@ const M2_NEX_BUTTON_MARKER_POSITIONS: Readonly> = { + left: { x: 0.39, y: 0.24 }, + right: { x: 0.61, y: 0.24 }, + middle: { x: 0.5, y: 0.31 }, + forward: { x: 0.27, y: 0.47 }, + backward: { x: 0.27, y: 0.59 }, + dpi: { x: 0.5, y: 0.42 }, +}; + function m2NexButtonMarkerPosition(button: string): ButtonMarkerPosition | null { return M2_NEX_BUTTON_MARKER_POSITIONS[button.trim().toLowerCase()] ?? null; } +function noirS1ButtonMarkerPosition(button: string): ButtonMarkerPosition | null { + return NOIR_S1_BUTTON_MARKER_POSITIONS[button.trim().toLowerCase()] ?? null; +} + // Device artwork requests go straight to the documented GitHub issue form // (public/devices/README.md, .github/ISSUE_TEMPLATE/device-artwork.yml). const ARTWORK_ISSUE_URL = "https://github.com/OpenMouse-Project/openmouse/issues/new?template=device-artwork.yml"; @@ -219,7 +232,10 @@ function DeviceShowcase({ snapshot }: { function M2NexProfileOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode { const status = snapshot.status; const locale = snapshot.preferences.locale; - if (status?.brand !== "Noir Gear" || status.name !== "M2-NEX" || snapshot.m2nexProfiles === null) return null; + const isNoirProfile = status?.brand === "Noir Gear" + && (status.name === "M2-NEX" || status.name === "NOIR S1"); + const profileImportRef = useRef(null); + if (!isNoirProfile || snapshot.m2nexProfiles === null) return null; const profile = snapshot.m2nexProfiles[snapshot.activeM2NexProfile]; if (!profile) return null; const applyDisabled = snapshot.settingInProgress || snapshot.pending.count > 0; @@ -250,6 +266,20 @@ function M2NexProfileOverview({ snapshot }: { snapshot: ControlSnapshot }): Reac {snapshot.m2nexProfileDirty ? t(locale, "m2nex.pending") : t(locale, "m2nex.saved")}
+ +
+ { + const file = event.currentTarget.files?.[0]; + event.currentTarget.value = ""; + if (!file) return; + void file.text().then((text) => control.importM2NexProfiles(text)); + }} + /> ); } @@ -275,9 +317,10 @@ export function DeviceShowcaseSidebar({ snapshot }: { snapshot: ControlSnapshot if (!status) return null; const locale = snapshot.preferences.locale; const image = snapshot.deviceArtwork; + const isNoirS1 = status.brand === "Noir Gear" && status.name === "NOIR S1"; const showButtonMarkers = snapshot.workspaceTab === "buttons" && status.brand === "Noir Gear" - && status.name === "M2-NEX" + && (status.name === "M2-NEX" || isNoirS1) && status.buttonMappings != null; return ( @@ -296,7 +339,9 @@ export function DeviceShowcaseSidebar({ snapshot }: { snapshot: ControlSnapshot />