Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/devices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,8 @@ package:
place and falls back to the placeholder until then. Not yet cleared for
licensing: it is vendor product art, so treat it as a request rather than an
approved asset.

`dareu-a950-pro-mg.png` was supplied by a contributor as a transparent top-down
render of the Dareu A950 PRO Mg. It is used locally for both the wired mouse
and its 2.4 GHz receiver. The image originated from Dareu's All in One Web
product artwork; confirm redistribution terms before a public release.
Binary file added public/devices/dareu-a950-pro-mg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 16 additions & 7 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
PulsarProCard,
SignalCard,
SleepCard,
TeevolutionDpiLightingCard,
DpiLightingCard,
} from "./cards/AdvancedCards";
import { cardAvailability } from "./cards/availability";
import type { MouseStatus } from "@openmouse/protocol/drivers";
Expand All @@ -51,7 +51,9 @@ function on(tab: WorkspaceTab, tabs: readonly WorkspaceTab[]): boolean {
function DeviceOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
if (!status) return null;
const has = cardAvailability(snapshot);
const locale = snapshot.preferences.locale;
const powerOverview = status.ui?.powerOverview === true;
const isWired = status.connectionType === "Wired";
const showBattery = !snapshot.traits.eggControls
&& (status.ui?.forceShowBattery || !isWired || status.batteryPercent !== null);
Expand All @@ -60,7 +62,7 @@ function DeviceOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
&& status.dongleLedEnabled !== null
&& status.dongleLedEnabled !== undefined;

return (
return <>
<section
id="device-overview"
className="device-overview device-data"
Expand Down Expand Up @@ -108,7 +110,13 @@ function DeviceOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
) : null}
</article>
</section>
);
{powerOverview && (has.teevolutionDpiLighting || has.sleep) ? (
<section id="power-overview-settings" className="settings-grid device-data" aria-label="Power settings">
{has.teevolutionDpiLighting ? <DpiLightingCard snapshot={snapshot} /> : null}
{has.sleep ? <SleepCard snapshot={snapshot} /> : null}
</section>
) : null}
</>;
}

function Workspace({
Expand All @@ -124,6 +132,7 @@ function Workspace({
const locale = snapshot.preferences.locale;
const has = cardAvailability(snapshot);
const show = (available: boolean, tabs: readonly WorkspaceTab[]): boolean => available && on(tab, tabs);
const powerOverview = status.ui?.powerOverview === true;

const performance = [
show(has.dpi, ["performance"]) ? <DpiCard key="dpi" snapshot={snapshot} /> : null,
Expand All @@ -135,7 +144,7 @@ function Workspace({
const advanced = [
show(has.signal, ["advanced"]) ? <SignalCard key="signal" snapshot={snapshot} /> : null,
show(has.debounce, ["buttons"]) ? <DebounceCard key="debounce" snapshot={snapshot} /> : null,
show(has.sleep, ["advanced"]) ? <SleepCard key="sleep" snapshot={snapshot} /> : null,
!powerOverview && show(has.sleep, ["advanced"]) ? <SleepCard key="sleep" snapshot={snapshot} /> : null,
show(has.lightingAdvanced, ["advanced"])
? <LightingCard key="lighting" snapshot={snapshot} variant="advanced" /> : null,
show(has.ninjutsoSensor, ["performance"])
Expand Down Expand Up @@ -169,8 +178,8 @@ function Workspace({
const lighting = [
show(has.lighting, ["lighting"])
? <LightingCard key="lighting-tab" snapshot={snapshot} variant="tab" zones={lightingZones} /> : null,
show(has.teevolutionDpiLighting, ["lighting"])
? <TeevolutionDpiLightingCard key="teevo" snapshot={snapshot} /> : null,
!powerOverview && show(has.teevolutionDpiLighting, ["lighting"])
? <DpiLightingCard key="dpi-indicator" snapshot={snapshot} /> : null,
].filter((node) => node !== null);

const showProfiles = show(has.profiles, ["profiles"]);
Expand Down Expand Up @@ -315,7 +324,7 @@ export function App(): ReactNode {
var tempTabs = WORKSPACE_TAB_ORDER;
const has = cardAvailability(snapshot);
if(status==null)return tempTabs;
if(!has.lighting&&!has.teevolutionDpiLighting)tempTabs=tempTabs.filter(tab=>tab!="lighting")
if(!has.lighting&&(!has.teevolutionDpiLighting||status.ui?.powerOverview===true))tempTabs=tempTabs.filter(tab=>tab!="lighting")
if(!has.eggButtons&&
!has.razerButtons&&
!has.mxMasterButtons&&
Expand Down
63 changes: 43 additions & 20 deletions src/app/cards/AdvancedCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,29 +428,35 @@ export function ProcessingCard({ snapshot }: { snapshot: ControlSnapshot }): Rea
);
}

export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
export function DpiLightingCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
const profile = snapshot.capabilities?.teevolutionProfile;
const profile = snapshot.capabilities?.dpiLighting;
if (!status || !profile) return null;
const locale = snapshot.preferences.locale;
const lightMode = status.dpiLedMode ?? 0;
const staged = snapshot.pending.keys.some((key) => key.startsWith("teevolution-dpi-light-"));
const teevolution = snapshot.traits.teevolution;
const powerOverview = status.ui?.powerOverview === true;
const staged = snapshot.pending.keys.some((key) => key.startsWith("teevolution-dpi-light-") || key.startsWith("dpi-light-"));
const apply = (setting: "mode" | "brightness" | "speed", value: number): void => {
if (teevolution) control.applyTeevolutionDpiLighting(setting, value);
else control.applyDpiLighting(setting, value);
};
return (
<article
id="teevolution-dpi-lighting"
className={`setting-card${staged ? " is-staged" : ""}`}
data-pending-key="teevolution-dpi-light-mode teevolution-dpi-light-brightness teevolution-dpi-light-speed"
data-pending-key="teevolution-dpi-light-mode teevolution-dpi-light-brightness teevolution-dpi-light-speed dpi-light-mode dpi-light-brightness dpi-light-speed dpi-light-sleep"
>
<div className="setting-heading compact"><div><p>LIGHTING</p><h2>{t(locale, "adv.dpiIndicator")}</h2></div></div>
<div className="setting-heading compact"><div><p>{powerOverview ? "POWER" : "LIGHTING"}</p><h2>{t(locale, "adv.dpiIndicator")}</h2></div></div>
<label className="field-label">
{t(locale, "adv.effect")}
<select
id="teevolution-dpi-light-mode"
value={lightMode}
onChange={(event) => control.applyTeevolutionDpiLighting("mode", Number(event.currentTarget.value))}
onChange={(event) => apply("mode", Number(event.currentTarget.value))}
>
{([[0, "adv.ledOff"], [1, "adv.ledSteady"], [2, "adv.ledBreathing"]] as const)
.filter(([value]) => profile.dpiLighting.modes.includes(value))
.filter(([value]) => profile.modes.includes(value))
.map(([value, label]) => <option key={value} value={value}>{t(locale, label)}</option>)}
</select>
</label>
Expand All @@ -464,16 +470,16 @@ export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnap
<input
id="teevolution-dpi-light-brightness"
type="range"
min={profile.dpiLighting.brightness.min}
max={profile.dpiLighting.brightness.max}
min={profile.brightness.min}
max={profile.brightness.max}
step={1}
value={status.dpiLedBrightness ?? profile.dpiLighting.brightness.min}
value={status.dpiLedBrightness ?? profile.brightness.min}
disabled={lightMode !== 1 || status.dpiLedBrightness == null}
style={{
"--fill": `${((status.dpiLedBrightness ?? profile.dpiLighting.brightness.min) - profile.dpiLighting.brightness.min)
/ Math.max(1, profile.dpiLighting.brightness.max - profile.dpiLighting.brightness.min) * 100}%`,
"--fill": `${((status.dpiLedBrightness ?? profile.brightness.min) - profile.brightness.min)
/ Math.max(1, profile.brightness.max - profile.brightness.min) * 100}%`,
}}
onChange={(event) => control.applyTeevolutionDpiLighting("brightness", Number(event.currentTarget.value))}
onChange={(event) => apply("brightness", Number(event.currentTarget.value))}
/>
</span>
</label>
Expand All @@ -486,25 +492,42 @@ export function TeevolutionDpiLightingCard({ snapshot }: { snapshot: ControlSnap
<input
id="teevolution-dpi-light-speed"
type="range"
min={profile.dpiLighting.speed.min}
max={profile.dpiLighting.speed.max}
min={profile.speed.min}
max={profile.speed.max}
step={1}
value={status.dpiLedSpeed ?? profile.dpiLighting.speed.min}
value={status.dpiLedSpeed ?? profile.speed.min}
disabled={lightMode !== 2 || status.dpiLedSpeed == null}
style={{
"--fill": `${((status.dpiLedSpeed ?? profile.dpiLighting.speed.min) - profile.dpiLighting.speed.min)
/ Math.max(1, profile.dpiLighting.speed.max - profile.dpiLighting.speed.min) * 100}%`,
"--fill": `${((status.dpiLedSpeed ?? profile.speed.min) - profile.speed.min)
/ Math.max(1, profile.speed.max - profile.speed.min) * 100}%`,
}}
onChange={(event) => control.applyTeevolutionDpiLighting("speed", Number(event.currentTarget.value))}
onChange={(event) => apply("speed", Number(event.currentTarget.value))}
/>
</span>
</label>
</div>
<small className="setting-note">{t(locale, "adv.dpiStageNote")}</small>
{profile.sleepTimeouts?.length && status.dpiLedSleepTimeout != null ? (
<label className="field-label spaced">
{t(locale, "adv.autoSleep")}
<select
id="dpi-light-sleep-timeout"
value={status.dpiLedSleepTimeout}
onChange={(event) => control.applyDpiLightingSleepTimeout(Number(event.currentTarget.value))}
>
{profile.sleepTimeouts.map((seconds) => (
<option key={seconds} value={seconds}>{sleepLabel(seconds, locale)}</option>
))}
</select>
</label>
) : null}
{teevolution ? <small className="setting-note">{t(locale, "adv.dpiStageNote")}</small> : null}
</article>
);
}

/** @deprecated Kept as an import alias for integrations built before generic DPI lighting. */
export const TeevolutionDpiLightingCard = DpiLightingCard;

export function NinjutsoSensorCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
if (!status) return null;
Expand Down
5 changes: 2 additions & 3 deletions src/app/cards/DpiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ function AxisControls({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
id="apply-logitech-axes"
className="axis-apply"
type="button"
onClick={() => control.applyLogitechAxisDpi(Number(x), Number(y))}
onClick={() => control.applySeparateDpiAxes(Number(x), Number(y))}
>
{t(locale, "common.apply")}
</button>
Expand All @@ -338,8 +338,7 @@ export function DpiCard({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
&& Array.isArray(status.dpiStages)
&& status.dpiStages.length > 0
&& !slotsAvailable;
const showSeparateDpiAxes = snapshot.traits.logitech
&& status.supportsSeparateDpiAxes === true
const showSeparateDpiAxes = status.supportsSeparateDpiAxes === true
&& !slotsAvailable;

const common = dpiPresetValues(snapshot.dpiOptions);
Expand Down
20 changes: 20 additions & 0 deletions src/app/cards/availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ test("ATK exposes its processing and DPI-lighting cards from reported controls",
assert.equal(has.teevolutionDpiLighting, true);
});

test("Dareu exposes its verified mouse and DPI-indicator sleep controls", () => {
const has = cardAvailability(snapshot({
status: {
brand: "Dareu",
ui: {
family: "dareu",
showAdvancedSection: true,
dpiLighting: {
modes: [0, 1, 2], brightness: [1, 2], speed: [1, 2], sleepTimeouts: [60, 0],
},
},
sleepTimeout: 180,
dpiLedSleepTimeout: 180,
},
}));
assert.equal(has.advancedHost, true);
assert.equal(has.sleep, true);
assert.equal(has.teevolutionDpiLighting, true);
});

test("ATK inspection cards require data actually read from the device", () => {
const empty = cardAvailability(snapshot({ status: { brand: "VXE", ui: { family: "atk" } } }));
assert.equal(empty.atkButtons, false);
Expand Down
7 changes: 3 additions & 4 deletions src/control-devices.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/control.css
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .2rem 0; color: var(--dim); font-size: .7rem; }
.field-label { display: block; color: var(--faint); font-size: .62rem; }
.field-label.spaced { margin-top: .35rem; }
#sleep-select { width: 100%; }
.teevolution-dpi-light-controls { margin-top: .55rem; }

.egg-collapsible, .egg-experimental { overflow: hidden; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--card); }
Expand Down Expand Up @@ -1140,6 +1141,7 @@ body { height: 100vh; overflow: hidden; }
.workspace-tab-empty small { display: block; margin-top: .45rem; color: var(--text-faint); font-size: .64rem; }

#performance-settings { order: 3; }
#power-overview-settings { order: 3; }
#lighting-settings { order: 4; }
#logitech-onboard, #nape-layers { order: 5; }
/* The MX Master feature cards are the primary Advanced-tab content; without an
Expand Down
Loading