tracky-mouse 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/images/head-not-found.svg +135 -0
- package/images/manual-takeback.svg +127 -0
- package/locales/ar/translation.json +197 -202
- package/locales/ar-EG/translation.json +197 -202
- package/locales/bg/translation.json +197 -202
- package/locales/bn/translation.json +197 -202
- package/locales/ca/translation.json +197 -202
- package/locales/ce/translation.json +197 -202
- package/locales/ceb/translation.json +197 -202
- package/locales/cs/translation.json +197 -202
- package/locales/da/translation.json +197 -202
- package/locales/de/translation.json +197 -202
- package/locales/el/translation.json +197 -202
- package/locales/emoji/translation.json +197 -202
- package/locales/en/translation.json +197 -202
- package/locales/eo/translation.json +197 -202
- package/locales/es/translation.json +197 -202
- package/locales/eu/translation.json +197 -202
- package/locales/fa/translation.json +197 -202
- package/locales/fi/translation.json +197 -202
- package/locales/fr/translation.json +197 -202
- package/locales/gu/translation.json +197 -202
- package/locales/ha/translation.json +197 -202
- package/locales/he/translation.json +197 -202
- package/locales/hi/translation.json +197 -202
- package/locales/hr/translation.json +197 -202
- package/locales/hu/translation.json +197 -202
- package/locales/hy/translation.json +197 -202
- package/locales/id/translation.json +197 -202
- package/locales/it/translation.json +197 -202
- package/locales/ja/translation.json +197 -202
- package/locales/jv/translation.json +197 -202
- package/locales/ko/translation.json +197 -202
- package/locales/mr/translation.json +197 -202
- package/locales/ms/translation.json +197 -202
- package/locales/nan/translation.json +197 -202
- package/locales/nb/translation.json +197 -202
- package/locales/nl/translation.json +197 -202
- package/locales/pa/translation.json +197 -202
- package/locales/pl/translation.json +197 -202
- package/locales/pt/translation.json +197 -202
- package/locales/pt-BR/translation.json +197 -202
- package/locales/ro/translation.json +197 -202
- package/locales/ru/translation.json +197 -202
- package/locales/sk/translation.json +197 -202
- package/locales/sl/translation.json +197 -202
- package/locales/sr/translation.json +197 -202
- package/locales/sv/translation.json +197 -202
- package/locales/sw/translation.json +197 -202
- package/locales/ta/translation.json +197 -202
- package/locales/te/translation.json +197 -202
- package/locales/th/translation.json +197 -202
- package/locales/tl/translation.json +197 -202
- package/locales/tr/translation.json +197 -202
- package/locales/tt/translation.json +197 -202
- package/locales/uk/translation.json +197 -202
- package/locales/ur/translation.json +197 -202
- package/locales/uz/translation.json +197 -202
- package/locales/vi/translation.json +197 -202
- package/locales/war/translation.json +197 -202
- package/locales/zh/translation.json +197 -202
- package/locales/zh-simplified/translation.json +198 -203
- package/package.json +2 -1
- package/tracky-mouse.css +72 -6
- package/tracky-mouse.js +300 -217
package/tracky-mouse.js
CHANGED
|
@@ -72,79 +72,79 @@ const initDwellClicking = (config) => {
|
|
|
72
72
|
*/
|
|
73
73
|
|
|
74
74
|
/** translation placeholder */
|
|
75
|
-
const t = (
|
|
75
|
+
const t = (key, options = {}) => options.defaultValue ?? key;
|
|
76
76
|
|
|
77
77
|
if (typeof config !== "object") {
|
|
78
|
-
throw new Error(t("configuration object required for initDwellClicking"));
|
|
78
|
+
throw new Error(t("api.errors.configRequired", { defaultValue: "configuration object required for initDwellClicking" }));
|
|
79
79
|
}
|
|
80
80
|
if (config.targets === undefined) {
|
|
81
|
-
throw new Error(t("config.targets is required (must be a CSS selector)"));
|
|
81
|
+
throw new Error(t("api.errors.targetsRequired", { defaultValue: "config.targets is required (must be a CSS selector)" }));
|
|
82
82
|
}
|
|
83
83
|
if (typeof config.targets !== "string") {
|
|
84
|
-
throw new Error(t("config.targets must be a string (a CSS selector)"));
|
|
84
|
+
throw new Error(t("api.errors.targetsMustBeSelectorString", { defaultValue: "config.targets must be a string (a CSS selector)" }));
|
|
85
85
|
}
|
|
86
86
|
if (!isSelectorValid(config.targets)) {
|
|
87
|
-
throw new Error(t("config.targets is not a valid CSS selector"));
|
|
87
|
+
throw new Error(t("api.errors.targetsInvalidSelector", { defaultValue: "config.targets is not a valid CSS selector" }));
|
|
88
88
|
}
|
|
89
89
|
if (config.click === undefined) {
|
|
90
|
-
throw new Error(t("config.click is required"));
|
|
90
|
+
throw new Error(t("api.errors.clickRequired", { defaultValue: "config.click is required" }));
|
|
91
91
|
}
|
|
92
92
|
if (typeof config.click !== "function") {
|
|
93
|
-
throw new Error(t("
|
|
93
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.click"));
|
|
94
94
|
}
|
|
95
95
|
if (config.shouldDrag !== undefined && typeof config.shouldDrag !== "function") {
|
|
96
|
-
throw new Error(t("
|
|
96
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.shouldDrag"));
|
|
97
97
|
}
|
|
98
98
|
if (config.noCenter !== undefined && typeof config.noCenter !== "function") {
|
|
99
|
-
throw new Error(t("
|
|
99
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.noCenter"));
|
|
100
100
|
}
|
|
101
101
|
if (config.isEquivalentTarget !== undefined && typeof config.isEquivalentTarget !== "function") {
|
|
102
|
-
throw new Error(t("
|
|
102
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.isEquivalentTarget"));
|
|
103
103
|
}
|
|
104
104
|
if (config.dwellClickEvenIfPaused !== undefined && typeof config.dwellClickEvenIfPaused !== "function") {
|
|
105
|
-
throw new Error(t("
|
|
105
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.dwellClickEvenIfPaused"));
|
|
106
106
|
}
|
|
107
107
|
if (config.beforeDispatch !== undefined && typeof config.beforeDispatch !== "function") {
|
|
108
|
-
throw new Error(t("
|
|
108
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.beforeDispatch"));
|
|
109
109
|
}
|
|
110
110
|
if (config.afterDispatch !== undefined && typeof config.afterDispatch !== "function") {
|
|
111
|
-
throw new Error(t("
|
|
111
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.afterDispatch"));
|
|
112
112
|
}
|
|
113
113
|
if (config.beforePointerDownDispatch !== undefined && typeof config.beforePointerDownDispatch !== "function") {
|
|
114
|
-
throw new Error(t("
|
|
114
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.beforePointerDownDispatch"));
|
|
115
115
|
}
|
|
116
116
|
if (config.isHeld !== undefined && typeof config.isHeld !== "function") {
|
|
117
|
-
throw new Error(t("
|
|
117
|
+
throw new Error(t("api.errors.functionRequired", { defaultValue: "%0 must be a function" }).replace("%0", "config.isHeld"));
|
|
118
118
|
}
|
|
119
119
|
if (config.retarget !== undefined) {
|
|
120
120
|
if (!Array.isArray(config.retarget)) {
|
|
121
|
-
throw new Error(t("config.retarget must be an array of objects"));
|
|
121
|
+
throw new Error(t("api.errors.retargetMustBeArray", { defaultValue: "config.retarget must be an array of objects" }));
|
|
122
122
|
}
|
|
123
123
|
for (let i = 0; i < config.retarget.length; i++) {
|
|
124
124
|
const rule = config.retarget[i];
|
|
125
125
|
if (typeof rule !== "object") {
|
|
126
|
-
throw new Error(t("config.retarget must be an array of objects"));
|
|
126
|
+
throw new Error(t("api.errors.retargetMustBeArray", { defaultValue: "config.retarget must be an array of objects" }));
|
|
127
127
|
}
|
|
128
128
|
if (rule.from === undefined) {
|
|
129
|
-
throw new Error(t("config.retarget[%0].from is required").replace("%0", i));
|
|
129
|
+
throw new Error(t("api.errors.retargetFromRequired", { defaultValue: "config.retarget[%0].from is required" }).replace("%0", i));
|
|
130
130
|
}
|
|
131
131
|
if (rule.to === undefined) {
|
|
132
|
-
throw new Error(t("config.retarget[%0].to is required (although can be null to ignore the element)").replace("%0", i));
|
|
132
|
+
throw new Error(t("api.errors.retargetToRequired", { defaultValue: "config.retarget[%0].to is required (although can be null to ignore the element)" }).replace("%0", i));
|
|
133
133
|
}
|
|
134
134
|
if (rule.withinMargin !== undefined && typeof rule.withinMargin !== "number") {
|
|
135
|
-
throw new Error(t("
|
|
135
|
+
throw new Error(t("api.errors.numberRequired", { defaultValue: "%0 must be a number" }).replace("%0", `config.retarget[${i}].withinMargin`));
|
|
136
136
|
}
|
|
137
137
|
if (typeof rule.from !== "string" && typeof rule.from !== "function" && !(rule.from instanceof Element)) {
|
|
138
|
-
throw new Error(t("config.retarget[%0].from must be a CSS selector string, an Element, or a function").replace("%0", i));
|
|
138
|
+
throw new Error(t("api.errors.retargetFromInvalidType", { defaultValue: "config.retarget[%0].from must be a CSS selector string, an Element, or a function" }).replace("%0", i));
|
|
139
139
|
}
|
|
140
140
|
if (typeof rule.to !== "string" && typeof rule.to !== "function" && !(rule.to instanceof Element) && rule.to !== null) {
|
|
141
|
-
throw new Error(t("config.retarget[%0].to must be a CSS selector string, an Element, a function, or null").replace("%0", i));
|
|
141
|
+
throw new Error(t("api.errors.retargetToInvalidType", { defaultValue: "config.retarget[%0].to must be a CSS selector string, an Element, a function, or null" }).replace("%0", i));
|
|
142
142
|
}
|
|
143
143
|
if (typeof rule.from === "string" && !isSelectorValid(rule.from)) {
|
|
144
|
-
throw new Error(t("config.retarget[%0].from is not a valid CSS selector").replace("%0", i));
|
|
144
|
+
throw new Error(t("api.errors.retargetFromInvalidSelector", { defaultValue: "config.retarget[%0].from is not a valid CSS selector" }).replace("%0", i));
|
|
145
145
|
}
|
|
146
146
|
if (typeof rule.to === "string" && !isSelectorValid(rule.to)) {
|
|
147
|
-
throw new Error(t("config.retarget[%0].to is not a valid CSS selector").replace("%0", i));
|
|
147
|
+
throw new Error(t("api.errors.retargetToInvalidSelector", { defaultValue: "config.retarget[%0].to is not a valid CSS selector" }).replace("%0", i));
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
@@ -571,7 +571,24 @@ TrackyMouse.cleanupDwellClicking = function () {
|
|
|
571
571
|
}
|
|
572
572
|
};
|
|
573
573
|
|
|
574
|
-
TrackyMouse._initInner = function (div,
|
|
574
|
+
TrackyMouse._initInner = function (div, initOptions, reinit) {
|
|
575
|
+
|
|
576
|
+
const {
|
|
577
|
+
statsJs = false,
|
|
578
|
+
// Unstable
|
|
579
|
+
updateInputFeedback = window.electronAPI?.updateInputFeedback,
|
|
580
|
+
// Unstable
|
|
581
|
+
setMouseButtonState = window.electronAPI?.setMouseButtonState,
|
|
582
|
+
// Unstable
|
|
583
|
+
notifyToggleState = window.electronAPI?.notifyToggleState,
|
|
584
|
+
// Unstable
|
|
585
|
+
handleSettingsUpdate,
|
|
586
|
+
// Unstable
|
|
587
|
+
clickingModeSupported = false,
|
|
588
|
+
// TODO: manage all of electronAPI similarly? well, setOptions is already a function in scope here,
|
|
589
|
+
// and it's not like we want to expose all electronAPI as part of the public API necessarily
|
|
590
|
+
// Could group things under an "unstable" object, or ideally, design nice APIs for everything.
|
|
591
|
+
} = initOptions;
|
|
575
592
|
|
|
576
593
|
const isDesktopApp = !!window.electronAPI;
|
|
577
594
|
|
|
@@ -617,7 +634,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
617
634
|
}
|
|
618
635
|
const rtlLanguages = ["ar", "he", "fa", "ur"]; // Right-to-left languages (current and future)
|
|
619
636
|
const isRTL = rtlLanguages.includes(locale.split("-")[0]);
|
|
620
|
-
const t = (
|
|
637
|
+
const t = (key, options = {}) => translations[key] ?? options.defaultValue ?? key;
|
|
621
638
|
// console.trace("Initializing UI with locale", locale);
|
|
622
639
|
|
|
623
640
|
// language name mappings marked with * may not be ISO 639-1
|
|
@@ -1594,21 +1611,21 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1594
1611
|
uiContainer.dir = isRTL ? "rtl" : "ltr";
|
|
1595
1612
|
uiContainer.innerHTML = `
|
|
1596
1613
|
<div class="tracky-mouse-controls">
|
|
1597
|
-
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("Start")}</button>
|
|
1614
|
+
<button class="tracky-mouse-start-stop-button" aria-pressed="false" aria-keyshortcuts="F9">${t("ui.startStopButton.start", { defaultValue: "Start" })}</button>
|
|
1598
1615
|
</div>
|
|
1599
1616
|
<div class="tracky-mouse-canvas-container-container">
|
|
1600
1617
|
<div class="tracky-mouse-canvas-container">
|
|
1601
1618
|
<div class="tracky-mouse-canvas-overlay">
|
|
1602
|
-
<button class="tracky-mouse-use-camera-button">${t("Allow Camera Access")}</button>
|
|
1603
|
-
<!--<button class="tracky-mouse-use-camera-button">${t("Use my camera")}</button>-->
|
|
1604
|
-
<button class="tracky-mouse-use-demo-footage-button" hidden>${t("Use demo footage")}</button>
|
|
1619
|
+
<button class="tracky-mouse-use-camera-button">${t("ui.camera.allowAccess", { defaultValue: "Allow Camera Access" })}</button>
|
|
1620
|
+
<!--<button class="tracky-mouse-use-camera-button">${t("ui.camera.useMyCamera", { defaultValue: "Use my camera" })}</button>-->
|
|
1621
|
+
<button class="tracky-mouse-use-demo-footage-button" hidden>${t("ui.camera.useDemoFootage", { defaultValue: "Use demo footage" })}</button>
|
|
1605
1622
|
<div class="tracky-mouse-error-message" role="alert" hidden></div>
|
|
1606
1623
|
</div>
|
|
1607
1624
|
<canvas class="tracky-mouse-canvas"></canvas>
|
|
1608
1625
|
</div>
|
|
1609
1626
|
</div>
|
|
1610
1627
|
<p class="tracky-mouse-desktop-app-download-message">
|
|
1611
|
-
${t('You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.')}
|
|
1628
|
+
${t("ui.desktopAppPromo.message", { defaultValue: 'You can control your entire computer with the <a href="https://trackymouse.js.org/">TrackyMouse</a> desktop app.' })}
|
|
1612
1629
|
</p>
|
|
1613
1630
|
`;
|
|
1614
1631
|
if (!div) {
|
|
@@ -1631,10 +1648,10 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1631
1648
|
const settingsCategories = [
|
|
1632
1649
|
{
|
|
1633
1650
|
type: "group",
|
|
1634
|
-
label: t("Cursor Movement"),
|
|
1651
|
+
label: t("settings.sections.cursorMovement.label", { defaultValue: "Cursor Movement" }),
|
|
1635
1652
|
settings: [
|
|
1636
1653
|
{
|
|
1637
|
-
label: t("Tilt influence"),
|
|
1654
|
+
label: t("settings.tiltInfluence.label", { defaultValue: "Tilt influence" }),
|
|
1638
1655
|
className: "tracky-mouse-tilt-influence",
|
|
1639
1656
|
key: "headTrackingTiltInfluence",
|
|
1640
1657
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -1644,20 +1661,21 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1644
1661
|
max: 100,
|
|
1645
1662
|
default: 0,
|
|
1646
1663
|
labels: {
|
|
1647
|
-
// min: t("Optical flow"), // too technical
|
|
1648
|
-
// min: t("Point tracking"), // still technical but at least it's terminology we're already using
|
|
1649
|
-
min: t("Point tracking (2D)"),
|
|
1650
|
-
// max: t("Head tilt"),
|
|
1651
|
-
max: t("Head tilt (3D)"),
|
|
1664
|
+
// min: t("settings.tiltInfluence.sliderMin.alt1", { defaultValue: "Optical flow" }), // too technical
|
|
1665
|
+
// min: t("settings.tiltInfluence.sliderMin.alt2", { defaultValue: "Point tracking" }), // still technical but at least it's terminology we're already using
|
|
1666
|
+
min: t("settings.tiltInfluence.sliderMin", { defaultValue: "Point tracking (2D)" }),
|
|
1667
|
+
// max: t("settings.tiltInfluence.sliderMax.alt1", { defaultValue: "Head tilt" }),
|
|
1668
|
+
max: t("settings.tiltInfluence.sliderMax", { defaultValue: "Head tilt (3D)" }),
|
|
1652
1669
|
},
|
|
1653
|
-
// description: t("Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed."),
|
|
1654
|
-
description: t(
|
|
1670
|
+
// description: t("settings.tiltInfluence.description.alt1", { defaultValue: "Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed." }),
|
|
1671
|
+
description: t("settings.tiltInfluence.description", {
|
|
1672
|
+
defaultValue: `Blends between using point tracking (2D) and detected head tilt (3D).
|
|
1655
1673
|
- At 0% it will use only point tracking. This moves the cursor according to visible movement of 2D points on your face within the camera's view, so it responds to both head rotation and translation.
|
|
1656
1674
|
- At 100% it will use only head tilt. This uses an estimate of your face's orientation in 3D space, and ignores head translation. Note that this is smoothed, so it's not as responsive as point tracking. In this mode you never need to recenter by pushing the cursor to the edge of the screen.
|
|
1657
|
-
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it.`),
|
|
1675
|
+
- In between it will behave like an automatic calibration, subtly adjusting the point tracking to match the head tilt. This works by slowing down mouse movement that is moving away from the position that would be expected based on the head tilt, and (only past 80% on the slider) actively moving towards it.` }),
|
|
1658
1676
|
},
|
|
1659
1677
|
{
|
|
1660
|
-
label: t("Motion threshold"),
|
|
1678
|
+
label: t("settings.motionThreshold.label", { defaultValue: "Motion threshold" }),
|
|
1661
1679
|
className: "tracky-mouse-min-distance",
|
|
1662
1680
|
key: "headTrackingMinDistance",
|
|
1663
1681
|
type: "slider",
|
|
@@ -1665,20 +1683,20 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1665
1683
|
max: 10,
|
|
1666
1684
|
default: 0,
|
|
1667
1685
|
labels: {
|
|
1668
|
-
min: t("Free"),
|
|
1669
|
-
max: t("Steady"),
|
|
1686
|
+
min: t("settings.motionThreshold.sliderMin", { defaultValue: "Free" }),
|
|
1687
|
+
max: t("settings.motionThreshold.sliderMax", { defaultValue: "Steady" }),
|
|
1670
1688
|
},
|
|
1671
|
-
description: t("Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor."),
|
|
1672
|
-
// description: t("Movement less than this distance in pixels will be ignored."),
|
|
1673
|
-
// description: t("Speed in pixels/frame required to move the cursor."),
|
|
1689
|
+
description: t("settings.motionThreshold.description", { defaultValue: "Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor." }),
|
|
1690
|
+
// description: t("settings.motionThreshold.description.alt1", { defaultValue: "Movement less than this distance in pixels will be ignored." }),
|
|
1691
|
+
// description: t("settings.motionThreshold.description.alt2", { defaultValue: "Speed in pixels/frame required to move the cursor." }),
|
|
1674
1692
|
},
|
|
1675
1693
|
{
|
|
1676
1694
|
type: "group",
|
|
1677
|
-
label: t("Point tracking"),
|
|
1695
|
+
label: t("settings.sections.pointTracking.label", { defaultValue: "Point tracking" }),
|
|
1678
1696
|
disabled: () => s.headTrackingTiltInfluence === 1,
|
|
1679
1697
|
settings: [
|
|
1680
1698
|
{
|
|
1681
|
-
label: t("Horizontal sensitivity"),
|
|
1699
|
+
label: t("settings.pointTracking.horizontalSensitivity.label", { defaultValue: "Horizontal sensitivity" }),
|
|
1682
1700
|
className: "tracky-mouse-sensitivity-x",
|
|
1683
1701
|
key: "headTrackingSensitivityX",
|
|
1684
1702
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -1688,13 +1706,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1688
1706
|
max: 100,
|
|
1689
1707
|
default: 25,
|
|
1690
1708
|
labels: {
|
|
1691
|
-
min: t("Slow"),
|
|
1692
|
-
max: t("Fast"),
|
|
1709
|
+
min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
|
|
1710
|
+
max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
|
|
1693
1711
|
},
|
|
1694
|
-
description: t("Speed of cursor movement in response to horizontal head movement."),
|
|
1712
|
+
description: t("settings.pointTracking.horizontalSensitivity.description", { defaultValue: "Speed of cursor movement in response to horizontal head movement." }),
|
|
1695
1713
|
},
|
|
1696
1714
|
{
|
|
1697
|
-
label: t("Vertical sensitivity"),
|
|
1715
|
+
label: t("settings.pointTracking.verticalSensitivity.label", { defaultValue: "Vertical sensitivity" }),
|
|
1698
1716
|
className: "tracky-mouse-sensitivity-y",
|
|
1699
1717
|
key: "headTrackingSensitivityY",
|
|
1700
1718
|
settingValueToInputValue: (settingValue) => settingValue * 1000,
|
|
@@ -1704,13 +1722,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1704
1722
|
max: 100,
|
|
1705
1723
|
default: 50,
|
|
1706
1724
|
labels: {
|
|
1707
|
-
min: t("Slow"),
|
|
1708
|
-
max: t("Fast"),
|
|
1725
|
+
min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
|
|
1726
|
+
max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
|
|
1709
1727
|
},
|
|
1710
|
-
description: t("Speed of cursor movement in response to vertical head movement."),
|
|
1728
|
+
description: t("settings.pointTracking.verticalSensitivity.description", { defaultValue: "Speed of cursor movement in response to vertical head movement." }),
|
|
1711
1729
|
},
|
|
1712
1730
|
// {
|
|
1713
|
-
// label: t("Smoothing"),
|
|
1731
|
+
// label: t("settings.pointTracking.smoothing.label", { defaultValue: "Smoothing" }),
|
|
1714
1732
|
// className: "tracky-mouse-smoothing",
|
|
1715
1733
|
// key: "headTrackingSmoothing",
|
|
1716
1734
|
// type: "slider",
|
|
@@ -1718,8 +1736,8 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1718
1736
|
// max: 100,
|
|
1719
1737
|
// default: 50,
|
|
1720
1738
|
// labels: {
|
|
1721
|
-
// min: t("Linear"), // or "Direct", "Raw", "None"
|
|
1722
|
-
// max: t("Smooth"), // or "Smoothed"
|
|
1739
|
+
// min: t("settings.shared.sliderMinLinear", { defaultValue: "Linear" }), // or "Direct", "Raw", "None"
|
|
1740
|
+
// max: t("settings.shared.sliderMaxSmooth", { defaultValue: "Smooth" }), // or "Smoothed"
|
|
1723
1741
|
// },
|
|
1724
1742
|
// },
|
|
1725
1743
|
|
|
@@ -1732,7 +1750,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1732
1750
|
// Should it be swapped? What does other software with acceleration control look like?
|
|
1733
1751
|
// In Windows it's just a checkbox apparently, but it could go as far as a custom curve editor.
|
|
1734
1752
|
{
|
|
1735
|
-
label: t("Acceleration"),
|
|
1753
|
+
label: t("settings.pointTracking.acceleration.label", { defaultValue: "Acceleration" }),
|
|
1736
1754
|
className: "tracky-mouse-acceleration",
|
|
1737
1755
|
key: "headTrackingAcceleration",
|
|
1738
1756
|
settingValueToInputValue: (settingValue) => settingValue * 100,
|
|
@@ -1742,23 +1760,24 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
|
|
|
1742
1760
|
max: 100,
|
|
1743
1761
|
default: 50,
|
|
1744
1762
|
labels: {
|
|
1745
|
-
min: t("Linear"), // or "Direct", "Raw"
|
|
1746
|
-
max: t("Smooth"),
|
|
1763
|
+
min: t("settings.shared.sliderMinLinear", { defaultValue: "Linear" }), // or "Direct", "Raw"
|
|
1764
|
+
max: t("settings.shared.sliderMaxSmooth", { defaultValue: "Smooth" }),
|
|
1747
1765
|
},
|
|
1748
|
-
// description: t("Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly."),
|
|
1749
|
-
// description: t("Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor."),
|
|
1750
|
-
description: t(
|
|
1751
|
-
|
|
1766
|
+
// description: t("settings.pointTracking.acceleration.description.alt1", { defaultValue: "Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly." }),
|
|
1767
|
+
// description: t("settings.pointTracking.acceleration.description.alt2", { defaultValue: "Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor." }),
|
|
1768
|
+
description: t("settings.pointTracking.acceleration.description", {
|
|
1769
|
+
defaultValue: `Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
|
|
1770
|
+
Helps to stabilize the cursor. However, when using point tracking in combination with head tilt, a lower value may work better since head tilt is linear, and you want the point tracking to roughly match the head tracking for it to act as a seamless auto-calibration.` }),
|
|
1752
1771
|
},
|
|
1753
1772
|
],
|
|
1754
1773
|
},
|
|
1755
1774
|
{
|
|
1756
1775
|
type: "group",
|
|
1757
|
-
label: t("Head tilt calibration"),
|
|
1776
|
+
label: t("settings.sections.headTiltCalibration.label", { defaultValue: "Head tilt calibration" }),
|
|
1758
1777
|
disabled: () => s.headTrackingTiltInfluence === 0,
|
|
1759
1778
|
settings: [
|
|
1760
1779
|
{
|
|
1761
|
-
label: t("Horizontal tilt range"),
|
|
1780
|
+
label: t("settings.headTilt.horizontalRange.label", { defaultValue: "Horizontal tilt range" }),
|
|
1762
1781
|
className: "tracky-mouse-head-tilt-yaw-range",
|
|
1763
1782
|
key: "headTiltYawRange",
|
|
1764
1783
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1768,16 +1787,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1768
1787
|
max: 90,
|
|
1769
1788
|
default: 60,
|
|
1770
1789
|
labels: {
|
|
1771
|
-
min: t("Little neck movement"),
|
|
1772
|
-
max: t("Large neck movement"),
|
|
1790
|
+
min: t("settings.headTilt.range.sliderMinLittleNeckMovement", { defaultValue: "Little neck movement" }),
|
|
1791
|
+
max: t("settings.headTilt.range.sliderMaxLargeNeckMovement", { defaultValue: "Large neck movement" }),
|
|
1773
1792
|
},
|
|
1774
|
-
// description: t("Range of horizontal head tilt that moves the cursor from one side of the screen to the other."),
|
|
1775
|
-
// description: t("How much you need to tilt your head left and right to reach the edges of the screen."),
|
|
1776
|
-
// description: t("How much you need to tilt your head left or right to reach the edge of the screen."),
|
|
1777
|
-
description: t("Controls how much you need to tilt your head left or right to reach the edge of the screen."),
|
|
1793
|
+
// description: t("settings.headTilt.horizontalRange.description.alt1", { defaultValue: "Range of horizontal head tilt that moves the cursor from one side of the screen to the other." }),
|
|
1794
|
+
// description: t("settings.headTilt.horizontalRange.description.alt2", { defaultValue: "How much you need to tilt your head left and right to reach the edges of the screen." }),
|
|
1795
|
+
// description: t("settings.headTilt.horizontalRange.description.alt3", { defaultValue: "How much you need to tilt your head left or right to reach the edge of the screen." }),
|
|
1796
|
+
description: t("settings.headTilt.horizontalRange.description", { defaultValue: "Controls how much you need to tilt your head left or right to reach the edge of the screen." }),
|
|
1778
1797
|
},
|
|
1779
1798
|
{
|
|
1780
|
-
label: t("Vertical tilt range"),
|
|
1799
|
+
label: t("settings.headTilt.verticalRange.label", { defaultValue: "Vertical tilt range" }),
|
|
1781
1800
|
className: "tracky-mouse-head-tilt-pitch-range",
|
|
1782
1801
|
key: "headTiltPitchRange",
|
|
1783
1802
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1787,17 +1806,17 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1787
1806
|
max: 60,
|
|
1788
1807
|
default: 25,
|
|
1789
1808
|
labels: {
|
|
1790
|
-
min: t("Little neck movement"),
|
|
1791
|
-
max: t("Large neck movement"),
|
|
1809
|
+
min: t("settings.headTilt.range.sliderMinLittleNeckMovement", { defaultValue: "Little neck movement" }),
|
|
1810
|
+
max: t("settings.headTilt.range.sliderMaxLargeNeckMovement", { defaultValue: "Large neck movement" }),
|
|
1792
1811
|
},
|
|
1793
|
-
// description: t("Range of vertical head tilt required to move the cursor from the top to the bottom of the screen."),
|
|
1794
|
-
// description: t("How much you need to tilt your head up and down to reach the edges of the screen."),
|
|
1795
|
-
// description: t("How much you need to tilt your head up or down to reach the edge of the screen."),
|
|
1796
|
-
description: t("Controls how much you need to tilt your head up or down to reach the edge of the screen."),
|
|
1812
|
+
// description: t("settings.headTilt.verticalRange.description.alt1", { defaultValue: "Range of vertical head tilt required to move the cursor from the top to the bottom of the screen." }),
|
|
1813
|
+
// description: t("settings.headTilt.verticalRange.description.alt2", { defaultValue: "How much you need to tilt your head up and down to reach the edges of the screen." }),
|
|
1814
|
+
// description: t("settings.headTilt.verticalRange.description.alt3", { defaultValue: "How much you need to tilt your head up or down to reach the edge of the screen." }),
|
|
1815
|
+
description: t("settings.headTilt.verticalRange.description", { defaultValue: "Controls how much you need to tilt your head up or down to reach the edge of the screen." }),
|
|
1797
1816
|
},
|
|
1798
1817
|
{
|
|
1799
1818
|
// label: "Horizontal tilt offset",
|
|
1800
|
-
label: t("Horizontal cursor offset"),
|
|
1819
|
+
label: t("settings.headTilt.horizontalOffset.label", { defaultValue: "Horizontal cursor offset" }),
|
|
1801
1820
|
className: "tracky-mouse-head-tilt-yaw-offset",
|
|
1802
1821
|
key: "headTiltYawOffset",
|
|
1803
1822
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1807,8 +1826,8 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1807
1826
|
max: 45,
|
|
1808
1827
|
default: 0,
|
|
1809
1828
|
labels: {
|
|
1810
|
-
min: t("Left"),
|
|
1811
|
-
max: t("Right"),
|
|
1829
|
+
min: t("settings.shared.directionLeft", { defaultValue: "Left" }),
|
|
1830
|
+
max: t("settings.shared.directionRight", { defaultValue: "Right" }),
|
|
1812
1831
|
},
|
|
1813
1832
|
// TODO: how to describe this??
|
|
1814
1833
|
// Specifically, how to disambiguate which direction is which / which way to adjust it?
|
|
@@ -1816,15 +1835,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1816
1835
|
// Since it's opposite, even though it's technically yaw (angle units), it's easier to think of as moving the cursor.
|
|
1817
1836
|
// Hence I've renamed the setting.
|
|
1818
1837
|
// A later update might change the definitions and include a settings file format upgrade step.
|
|
1819
|
-
// description: t("Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible."),
|
|
1820
|
-
// description: t("Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible."),
|
|
1838
|
+
// description: t("settings.headTilt.horizontalOffset.description.alt1", { defaultValue: "Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible." }),
|
|
1839
|
+
// description: t("settings.headTilt.horizontalOffset.description.alt2", { defaultValue: "Adjusts the center position of horizontal head tilt. This horizontal offset is not recommended. Move the camera instead if possible." }),
|
|
1821
1840
|
// TODO: should this say "horizontal" in the (main part of the) description?
|
|
1822
|
-
description: t(
|
|
1823
|
-
|
|
1841
|
+
description: t("settings.headTilt.horizontalOffset.description", {
|
|
1842
|
+
defaultValue: `Adjusts the position of the cursor when the camera sees the head facing straight ahead.
|
|
1843
|
+
⚠️ This horizontal offset is not recommended. Move the camera instead if possible. 📷` }),
|
|
1824
1844
|
},
|
|
1825
1845
|
{
|
|
1826
1846
|
// label: "Vertical tilt offset",
|
|
1827
|
-
label: t("Vertical cursor offset"),
|
|
1847
|
+
label: t("settings.headTilt.verticalOffset.label", { defaultValue: "Vertical cursor offset" }),
|
|
1828
1848
|
className: "tracky-mouse-head-tilt-pitch-offset",
|
|
1829
1849
|
key: "headTiltPitchOffset",
|
|
1830
1850
|
settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
|
|
@@ -1834,11 +1854,11 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1834
1854
|
max: 30,
|
|
1835
1855
|
default: 2.5,
|
|
1836
1856
|
labels: {
|
|
1837
|
-
min: t("Down"),
|
|
1838
|
-
max: t("Up"),
|
|
1857
|
+
min: t("settings.shared.directionDown", { defaultValue: "Down" }),
|
|
1858
|
+
max: t("settings.shared.directionUp", { defaultValue: "Up" }),
|
|
1839
1859
|
},
|
|
1840
|
-
// description: t("Adjusts the center position of vertical head tilt."),
|
|
1841
|
-
description: t("Adjusts the position of the cursor when the camera sees the head facing straight ahead."),
|
|
1860
|
+
// description: t("settings.headTilt.verticalOffset.description.alt1", { defaultValue: "Adjusts the center position of vertical head tilt." }),
|
|
1861
|
+
description: t("settings.headTilt.verticalOffset.description", { defaultValue: "Adjusts the position of the cursor when the camera sees the head facing straight ahead." }),
|
|
1842
1862
|
},
|
|
1843
1863
|
],
|
|
1844
1864
|
},
|
|
@@ -1858,42 +1878,43 @@ Helps to stabilize the cursor. However, when using point tracking in combination
|
|
|
1858
1878
|
// which awkwardly affects what mouse button serenade-driver sends; this doesn't affect the web version.
|
|
1859
1879
|
{
|
|
1860
1880
|
type: "group",
|
|
1861
|
-
label: t("Clicking"),
|
|
1881
|
+
label: t("settings.sections.clicking.label", { defaultValue: "Clicking" }),
|
|
1862
1882
|
settings: [
|
|
1863
1883
|
{
|
|
1864
|
-
label: t("Clicking mode:"), // TODO: ":"?
|
|
1884
|
+
label: t("settings.clickingMode.label", { defaultValue: "Clicking mode:" }), // TODO: ":"?
|
|
1865
1885
|
className: "tracky-mouse-clicking-mode",
|
|
1866
1886
|
key: "clickingMode",
|
|
1867
1887
|
type: "dropdown",
|
|
1868
1888
|
options: [
|
|
1869
|
-
{ value: "dwell", label: t("Dwell to click"), description: t("Hold the cursor in place for a short time to click.") },
|
|
1870
|
-
{ value: "blink", label: t("Wink to click"), description: t("Close one eye to click. Left eye for left click, right eye for right click.") },
|
|
1889
|
+
{ value: "dwell", label: t("settings.clickingMode.dwell.label", { defaultValue: "Dwell to click" }), description: t("settings.clickingMode.dwell.description", { defaultValue: "Hold the cursor in place for a short time to click." }) },
|
|
1890
|
+
{ value: "blink", label: t("settings.clickingMode.wink.label", { defaultValue: "Wink to click" }), description: t("settings.clickingMode.wink.description", { defaultValue: "Close one eye to click. Left eye for left click, right eye for right click." }) },
|
|
1871
1891
|
// TODO: clarify that ooh works better than ah
|
|
1872
1892
|
// "open wide" refers to height, but could be misinterpreted as opposite advice - a wide mouth shape when narrow works better
|
|
1873
1893
|
// "open wide" is also perhaps unnecessary considering detection is improved... but who knows. maybe someone will try opening their mouth only slightly and expect it to work
|
|
1874
1894
|
// Some people may understand "tall and narrow" better than "ooh rather than ah" and visa-versa
|
|
1875
|
-
{ value: "open-mouth-simple", label: t("Open mouth to click (simple)"), description: t("Open your mouth wide to click. At least one eye must be open to click.") },
|
|
1876
|
-
{ value: "open-mouth-ignoring-eyes", label: t("Open mouth to click (ignoring eyes)"), description: t("Open your mouth wide to click. Eye state is ignored.") },
|
|
1877
|
-
{ value: "open-mouth", label: t("Open mouth to click (with eye modifiers)"), description: t("Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click.") },
|
|
1878
|
-
{ value: "off", label: t("Off"), description: t("Disable clicking. Use with an external switch or programs that provide their own dwell clicking.") },
|
|
1895
|
+
{ value: "open-mouth-simple", label: t("settings.clickingMode.openMouthSimple.label", { defaultValue: "Open mouth to click (simple)" }), description: t("settings.clickingMode.openMouthSimple.description", { defaultValue: "Open your mouth wide to click. At least one eye must be open to click." }) },
|
|
1896
|
+
{ value: "open-mouth-ignoring-eyes", label: t("settings.clickingMode.openMouthIgnoringEyes.label", { defaultValue: "Open mouth to click (ignoring eyes)" }), description: t("settings.clickingMode.openMouthIgnoringEyes.description", { defaultValue: "Open your mouth wide to click. Eye state is ignored." }) },
|
|
1897
|
+
{ value: "open-mouth", label: t("settings.clickingMode.openMouthWithEyeModifiers.label", { defaultValue: "Open mouth to click (with eye modifiers)" }), description: t("settings.clickingMode.openMouthWithEyeModifiers.description", { defaultValue: "Open your mouth wide to click. If left eye is closed, it's a right click; if right eye is closed, it's a middle click." }) },
|
|
1898
|
+
{ value: "off", label: t("settings.clickingMode.off.label", { defaultValue: "Off" }), description: t("settings.clickingMode.off.description", { defaultValue: "Disable clicking. Use with an external switch or programs that provide their own dwell clicking." }) },
|
|
1879
1899
|
],
|
|
1880
1900
|
default: "dwell",
|
|
1881
|
-
visible: () => isDesktopApp,
|
|
1882
|
-
description: t("Choose how to perform mouse clicks."),
|
|
1901
|
+
visible: () => isDesktopApp || clickingModeSupported,
|
|
1902
|
+
description: t("settings.clickingMode.description", { defaultValue: "Choose how to perform mouse clicks." }),
|
|
1883
1903
|
},
|
|
1884
1904
|
{
|
|
1885
1905
|
// on Windows, currently, when buttons are swapped at the system level, it affects serenade-driver's click()
|
|
1886
1906
|
// "swap" is purposefully generic language so we don't have to know what system-level setting is
|
|
1887
1907
|
// (also this may be seen as a weirdly named/designed option for right-clicking with the dwell clicker)
|
|
1888
|
-
label: t("Swap mouse buttons"),
|
|
1908
|
+
label: t("settings.swapMouseButtons.label", { defaultValue: "Swap mouse buttons" }),
|
|
1889
1909
|
className: "tracky-mouse-swap-mouse-buttons",
|
|
1890
1910
|
key: "swapMouseButtons",
|
|
1891
1911
|
type: "checkbox",
|
|
1892
1912
|
default: false,
|
|
1893
1913
|
visible: () => isDesktopApp,
|
|
1894
|
-
description: t(
|
|
1914
|
+
description: t("settings.swapMouseButtons.description", {
|
|
1915
|
+
defaultValue: `Switches the left and right mouse buttons.
|
|
1895
1916
|
Useful if your system's mouse buttons are swapped.
|
|
1896
|
-
Could also be used to right click with the dwell clicker in a pinch.`),
|
|
1917
|
+
Could also be used to right click with the dwell clicker in a pinch.` }),
|
|
1897
1918
|
},
|
|
1898
1919
|
|
|
1899
1920
|
// This setting could called "click stabilization", "drag delay", "delay before dragging", "click drag delay", "drag prevention", etc.
|
|
@@ -1902,33 +1923,37 @@ Could also be used to right click with the dwell clicker in a pinch.`),
|
|
|
1902
1923
|
// at the end of the slider, although you shouldn't need to do that to effectively avoid dragging when trying to click,
|
|
1903
1924
|
// and it might complicate the design of the slider labeling.
|
|
1904
1925
|
{
|
|
1905
|
-
label: t("
|
|
1926
|
+
label: t("settings.delayBeforeDragging.label", { defaultValue: "Delay before dragging" }),
|
|
1906
1927
|
className: "tracky-mouse-delay-before-dragging",
|
|
1907
1928
|
key: "delayBeforeDragging",
|
|
1908
1929
|
type: "slider",
|
|
1909
1930
|
min: 0,
|
|
1910
1931
|
max: 1000,
|
|
1911
1932
|
labels: {
|
|
1912
|
-
min: t("Easy to drag"),
|
|
1913
|
-
max: t("Easy to click"),
|
|
1933
|
+
min: t("settings.delayBeforeDragging.sliderMin", { defaultValue: "Easy to drag" }),
|
|
1934
|
+
max: t("settings.delayBeforeDragging.sliderMax", { defaultValue: "Easy to click" }),
|
|
1914
1935
|
},
|
|
1915
1936
|
default: 800,
|
|
1916
|
-
visible: () => isDesktopApp,
|
|
1937
|
+
visible: () => isDesktopApp || clickingModeSupported,
|
|
1917
1938
|
disabled: () => s.clickingMode === "off" || s.clickingMode === "dwell",
|
|
1918
|
-
// description: t("Locks mouse movement during the start of a click to prevent accidental dragging."),
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1939
|
+
// description: t("settings.delayBeforeDragging.description.alt1", { defaultValue: "Locks mouse movement during the start of a click to prevent accidental dragging." }),
|
|
1940
|
+
// Throwing a // in here so it's not detected by i18next-cli, whereas the others are allowed
|
|
1941
|
+
// simply because it wasn't previously detected and translated
|
|
1942
|
+
// due to being both commented out and multiline (though multiline and commented out t() calls are separately supported)
|
|
1943
|
+
// description: t//("settings.delayBeforeDragging.description.alt2", { defaultValue: `Prevents mouse movement for the specified time after a click starts.
|
|
1944
|
+
// You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.` }),
|
|
1945
|
+
description: t("settings.delayBeforeDragging.description", {
|
|
1946
|
+
defaultValue: `Locks mouse movement for the given duration during the start of a click.
|
|
1947
|
+
You may want to turn this off if you're drawing on a canvas, or increase it if you find yourself accidentally dragging when you try to click.` }),
|
|
1923
1948
|
},
|
|
1924
1949
|
],
|
|
1925
1950
|
},
|
|
1926
1951
|
{
|
|
1927
1952
|
type: "group",
|
|
1928
|
-
label: t("Video"),
|
|
1953
|
+
label: t("settings.sections.video.label", { defaultValue: "Video" }),
|
|
1929
1954
|
settings: [
|
|
1930
1955
|
{
|
|
1931
|
-
label: t("Camera source"),
|
|
1956
|
+
label: t("settings.cameraSource.label", { defaultValue: "Camera source" }),
|
|
1932
1957
|
className: "tracky-mouse-camera-select",
|
|
1933
1958
|
key: "cameraDeviceId",
|
|
1934
1959
|
handleSettingChange: () => {
|
|
@@ -1936,15 +1961,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1936
1961
|
},
|
|
1937
1962
|
type: "dropdown",
|
|
1938
1963
|
options: [
|
|
1939
|
-
{ value: "", label: t("Default") },
|
|
1964
|
+
{ value: "", label: t("settings.cameraSource.defaultCamera", { defaultValue: "Default" }) },
|
|
1940
1965
|
],
|
|
1941
1966
|
default: "",
|
|
1942
|
-
// description: t("Select which camera to use for head tracking."),
|
|
1943
|
-
description: t("Selects which camera is used for head tracking."),
|
|
1967
|
+
// description: t("settings.cameraSource.description.alt1", { defaultValue: "Select which camera to use for head tracking." }),
|
|
1968
|
+
description: t("settings.cameraSource.description", { defaultValue: "Selects which camera is used for head tracking." }),
|
|
1944
1969
|
},
|
|
1945
1970
|
// TODO: move this inline with the camera source dropdown?
|
|
1946
1971
|
{
|
|
1947
|
-
label: t("Open Camera Settings"),
|
|
1972
|
+
label: t("settings.openCameraSettings.label", { defaultValue: "Open Camera Settings" }),
|
|
1948
1973
|
className: "tracky-mouse-open-camera-settings",
|
|
1949
1974
|
key: "openCameraSettings",
|
|
1950
1975
|
type: "button",
|
|
@@ -1954,45 +1979,45 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
1954
1979
|
try {
|
|
1955
1980
|
knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
|
|
1956
1981
|
} catch (error) {
|
|
1957
|
-
alert(t("Failed to open camera settings
|
|
1982
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + t("openCameraSettings.errors.parseKnownCameras", { defaultValue: "Failed to parse known cameras from localStorage:" }) + "\n" + error.message);
|
|
1958
1983
|
return;
|
|
1959
1984
|
}
|
|
1960
1985
|
|
|
1961
1986
|
const activeStream = cameraVideo.srcObject;
|
|
1962
1987
|
const activeDeviceId = activeStream?.getVideoTracks()[0]?.getSettings()?.deviceId;
|
|
1963
|
-
const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("Default");
|
|
1988
|
+
const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
|
|
1964
1989
|
|
|
1965
1990
|
try {
|
|
1966
1991
|
const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
|
|
1967
1992
|
if (result?.error) {
|
|
1968
|
-
alert(t("Failed to open camera settings
|
|
1993
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + result.error);
|
|
1969
1994
|
}
|
|
1970
1995
|
} catch (error) {
|
|
1971
|
-
alert(t("Failed to open camera settings
|
|
1996
|
+
alert(t("openCameraSettings.errors.sharedHeading", { defaultValue: "Failed to open camera settings:" }) + "\n" + error.message);
|
|
1972
1997
|
}
|
|
1973
1998
|
},
|
|
1974
|
-
// description: t("Open your camera's system settings window to adjust properties like brightness and contrast."),
|
|
1975
|
-
// description: t("Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure."),
|
|
1976
|
-
description: t("Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure."),
|
|
1999
|
+
// description: t("settings.openCameraSettings.description.alt1", { defaultValue: "Open your camera's system settings window to adjust properties like brightness and contrast." }),
|
|
2000
|
+
// description: t("settings.openCameraSettings.description.alt2", { defaultValue: "Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure." }),
|
|
2001
|
+
description: t("settings.openCameraSettings.description", { defaultValue: "Opens the system settings dialog for the selected camera, to adjust properties like auto-focus and auto-exposure." }),
|
|
1977
2002
|
},
|
|
1978
2003
|
// TODO: try moving this to the corner of the camera view, so it's clearer it applies only to the camera view
|
|
1979
2004
|
{
|
|
1980
|
-
label: t("Mirror"),
|
|
2005
|
+
label: t("settings.mirror.label", { defaultValue: "Mirror" }),
|
|
1981
2006
|
className: "tracky-mouse-mirror",
|
|
1982
2007
|
key: "mirror",
|
|
1983
2008
|
type: "checkbox",
|
|
1984
2009
|
default: true,
|
|
1985
|
-
description: t("Mirrors the camera view horizontally."),
|
|
2010
|
+
description: t("settings.mirror.description", { defaultValue: "Mirrors the camera view horizontally." }),
|
|
1986
2011
|
},
|
|
1987
2012
|
]
|
|
1988
2013
|
},
|
|
1989
2014
|
{
|
|
1990
2015
|
type: "group",
|
|
1991
|
-
label: t("General"),
|
|
2016
|
+
label: t("settings.sections.general.label", { defaultValue: "General" }),
|
|
1992
2017
|
settings: [
|
|
1993
2018
|
// opposite, "Start paused", might be clearer, especially if I add a "pause" button
|
|
1994
2019
|
{
|
|
1995
|
-
label: t("Start enabled"),
|
|
2020
|
+
label: t("settings.startEnabled.label", { defaultValue: "Start enabled" }),
|
|
1996
2021
|
className: "tracky-mouse-start-enabled",
|
|
1997
2022
|
key: "startEnabled",
|
|
1998
2023
|
afterInitialLoad: () => { // TODO: does this hook make sense? right now it's the only usage. could this code not just be called later?
|
|
@@ -2000,10 +2025,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2000
2025
|
},
|
|
2001
2026
|
type: "checkbox",
|
|
2002
2027
|
default: false,
|
|
2003
|
-
description: t("If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched."),
|
|
2004
|
-
// description: t("Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready."),
|
|
2005
|
-
// description: t("Makes Tracky Mouse active as soon as it's launched."),
|
|
2006
|
-
// description: t("Automatically starts Tracky Mouse as soon as it's run."),
|
|
2028
|
+
description: t("settings.startEnabled.description", { defaultValue: "If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched." }),
|
|
2029
|
+
// description: t("settings.startEnabled.description.alt1", { defaultValue: "Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready." }),
|
|
2030
|
+
// description: t("settings.startEnabled.description.alt2", { defaultValue: "Makes Tracky Mouse active as soon as it's launched." }),
|
|
2031
|
+
// description: t("settings.startEnabled.description.alt3", { defaultValue: "Automatically starts Tracky Mouse as soon as it's run." }),
|
|
2007
2032
|
},
|
|
2008
2033
|
{
|
|
2009
2034
|
// For "experimental" label:
|
|
@@ -2011,36 +2036,36 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2011
2036
|
// - I considered adding "⚠︎" but it feels a little too alarming
|
|
2012
2037
|
// label: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"Planned refinements include: visual and auditory feedback, improved detection accuracy, and separate settings for durations to toggle on and off.\">experimental</span>)",
|
|
2013
2038
|
// label: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• Missing visual and auditory feedback.\n• Missing settings for duration(s) to toggle on and off.\n• Affected by false positive blink detections, especially when looking downward.\">Experimental</span>)",
|
|
2014
|
-
label: t("Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• There is currently no visual or auditory feedback.\n• There are no settings for duration(s) to toggle on and off.\n• It is affected by false positive blink detections, especially when looking downward.\">Experimental</span>)"),
|
|
2039
|
+
label: t("settings.closeEyesToToggle.label", { defaultValue: "Close eyes to start/stop (<span style=\"border-bottom: 1px dotted;\" title=\"• There is currently no visual or auditory feedback.\n• There are no settings for duration(s) to toggle on and off.\n• It is affected by false positive blink detections, especially when looking downward.\">Experimental</span>)" }),
|
|
2015
2040
|
className: "tracky-mouse-close-eyes-to-toggle",
|
|
2016
2041
|
key: "closeEyesToToggle",
|
|
2017
2042
|
type: "checkbox",
|
|
2018
2043
|
default: false,
|
|
2019
|
-
description: t("If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds."),
|
|
2044
|
+
description: t("settings.closeEyesToToggle.description", { defaultValue: "If enabled, you can start or stop mouse control by holding both your eyes shut for a few seconds." }),
|
|
2020
2045
|
},
|
|
2021
2046
|
{
|
|
2022
|
-
label: t("Run at login"),
|
|
2047
|
+
label: t("settings.runAtLogin.label", { defaultValue: "Run at login" }),
|
|
2023
2048
|
className: "tracky-mouse-run-at-login",
|
|
2024
2049
|
key: "runAtLogin",
|
|
2025
2050
|
type: "checkbox",
|
|
2026
2051
|
default: false,
|
|
2027
2052
|
visible: () => isDesktopApp,
|
|
2028
|
-
description: t("If enabled, Tracky Mouse will automatically start when you log into your computer."),
|
|
2029
|
-
// description: t("Makes Tracky Mouse start automatically when you log into your computer."),
|
|
2053
|
+
description: t("settings.runAtLogin.description", { defaultValue: "If enabled, Tracky Mouse will automatically start when you log into your computer." }),
|
|
2054
|
+
// description: t("settings.runAtLogin.description.alt1", { defaultValue: "Makes Tracky Mouse start automatically when you log into your computer." }),
|
|
2030
2055
|
},
|
|
2031
2056
|
{
|
|
2032
|
-
label: t("Check for updates"),
|
|
2057
|
+
label: t("settings.checkForUpdates.label", { defaultValue: "Check for updates" }),
|
|
2033
2058
|
className: "tracky-mouse-check-for-updates",
|
|
2034
2059
|
key: "checkForUpdates",
|
|
2035
2060
|
type: "checkbox",
|
|
2036
2061
|
default: true,
|
|
2037
2062
|
visible: () => isDesktopApp,
|
|
2038
|
-
description: t("If enabled, Tracky Mouse will automatically check for updates when it starts."),
|
|
2039
|
-
// description: t("Notifies you of new versions of Tracky Mouse."),
|
|
2040
|
-
// description: t("Notifies you when a new version of Tracky Mouse is available."),
|
|
2063
|
+
description: t("settings.checkForUpdates.description", { defaultValue: "If enabled, Tracky Mouse will automatically check for updates when it starts." }),
|
|
2064
|
+
// description: t("settings.checkForUpdates.description.alt1", { defaultValue: "Notifies you of new versions of Tracky Mouse." }),
|
|
2065
|
+
// description: t("settings.checkForUpdates.description.alt2", { defaultValue: "Notifies you when a new version of Tracky Mouse is available." }),
|
|
2041
2066
|
},
|
|
2042
2067
|
{
|
|
2043
|
-
label: t("Language"),
|
|
2068
|
+
label: t("settings.language.label", { defaultValue: "Language" }),
|
|
2044
2069
|
className: "tracky-mouse-language",
|
|
2045
2070
|
key: "language",
|
|
2046
2071
|
type: "dropdown",
|
|
@@ -2058,8 +2083,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2058
2083
|
}
|
|
2059
2084
|
reinit();
|
|
2060
2085
|
},
|
|
2061
|
-
description: t("Select the language for the Tracky Mouse interface."),
|
|
2062
|
-
// description: t("Changes the language Tracky Mouse is displayed in."),
|
|
2086
|
+
description: t("settings.language.description", { defaultValue: "Select the language for the Tracky Mouse interface." }),
|
|
2087
|
+
// description: t("settings.language.description.alt1", { defaultValue: "Changes the language Tracky Mouse is displayed in." }),
|
|
2063
2088
|
},
|
|
2064
2089
|
],
|
|
2065
2090
|
},
|
|
@@ -2181,7 +2206,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2181
2206
|
</select>
|
|
2182
2207
|
`;
|
|
2183
2208
|
if (setting.options.some(option => option.description)) {
|
|
2184
|
-
setting.description +=
|
|
2209
|
+
setting.description += "\n\n" + t("settings.dropdownDescriptionOptionsListHeading", { defaultValue: "Options:" }) + "\n" + setting.options.map(option => `• ${option.label}${option.description ? `: ${option.description}` : ''}`).join("\n");
|
|
2185
2210
|
}
|
|
2186
2211
|
} else if (setting.type === "button") {
|
|
2187
2212
|
rowEl.innerHTML = `
|
|
@@ -2495,6 +2520,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2495
2520
|
func();
|
|
2496
2521
|
}
|
|
2497
2522
|
|
|
2523
|
+
// Unstable hook
|
|
2524
|
+
handleSettingsUpdate?.(settings);
|
|
2498
2525
|
}
|
|
2499
2526
|
const formatVersion = 1;
|
|
2500
2527
|
const formatName = "tracky-mouse-settings";
|
|
@@ -2523,25 +2550,43 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2523
2550
|
console.error(e);
|
|
2524
2551
|
}
|
|
2525
2552
|
}
|
|
2553
|
+
// Unstable hook
|
|
2554
|
+
handleSettingsUpdate?.(options);
|
|
2526
2555
|
};
|
|
2527
2556
|
const loadOptions = async (initialLoad = false) => {
|
|
2557
|
+
// Desktop app: start from any saved settings in the main process,
|
|
2558
|
+
// then, on first load, push the renderer's canonical defaults back
|
|
2559
|
+
// so the main process has the same effective settings (and can
|
|
2560
|
+
// correctly drive features like dwell clicking on first run).
|
|
2561
|
+
// Web demo: similarly needs canonical defaults pushed to
|
|
2562
|
+
// correctly enable dwell clicking on first run,
|
|
2563
|
+
// now that it supports multiple clicking modes.
|
|
2564
|
+
// General API usage: does not yet support multiple clicking modes
|
|
2565
|
+
// (there's a lot of glue code in the demo)
|
|
2566
|
+
// but we only call handleSettingsUpdate if it exists.
|
|
2567
|
+
let stored;
|
|
2528
2568
|
if (window.electronAPI) {
|
|
2529
|
-
|
|
2530
|
-
// then, on first load, push the renderer's canonical defaults back
|
|
2531
|
-
// so the main process has the same effective settings (and can
|
|
2532
|
-
// correctly drive features like dwell clicking on first run).
|
|
2533
|
-
const stored = await window.electronAPI.getOptions();
|
|
2534
|
-
deserializeSettings(stored, initialLoad);
|
|
2535
|
-
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2536
|
-
setOptions(serializeSettings());
|
|
2537
|
-
}
|
|
2569
|
+
stored = await window.electronAPI.getOptions();
|
|
2538
2570
|
} else {
|
|
2539
2571
|
try {
|
|
2540
2572
|
if (localStorage.getItem("tracky-mouse-settings")) {
|
|
2541
|
-
|
|
2573
|
+
stored = JSON.parse(localStorage.getItem("tracky-mouse-settings"));
|
|
2542
2574
|
}
|
|
2543
2575
|
} catch (e) {
|
|
2544
2576
|
console.error(e);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
if (stored) {
|
|
2581
|
+
deserializeSettings(stored, initialLoad);
|
|
2582
|
+
}
|
|
2583
|
+
if (initialLoad && (!stored || !stored.globalSettings || Object.keys(stored.globalSettings).length === 0)) {
|
|
2584
|
+
// We could just call setOptions in both cases,
|
|
2585
|
+
// but do we want to save to localStorage initially? Maybe not.
|
|
2586
|
+
if (window.electronAPI) {
|
|
2587
|
+
setOptions(serializeSettings()); // (includes handleSettingsUpdate)
|
|
2588
|
+
} else {
|
|
2589
|
+
handleSettingsUpdate?.(serializeSettings());
|
|
2545
2590
|
}
|
|
2546
2591
|
}
|
|
2547
2592
|
};
|
|
@@ -2592,14 +2637,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2592
2637
|
|
|
2593
2638
|
const defaultOption = document.createElement("option");
|
|
2594
2639
|
defaultOption.value = "";
|
|
2595
|
-
defaultOption.text = t("Default");
|
|
2640
|
+
defaultOption.text = t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
|
|
2596
2641
|
cameraSelect.appendChild(defaultOption);
|
|
2597
2642
|
|
|
2598
2643
|
let matchingDeviceId = "";
|
|
2599
2644
|
for (const device of videoDevices) {
|
|
2600
2645
|
const option = document.createElement('option');
|
|
2601
2646
|
option.value = device.deviceId;
|
|
2602
|
-
option.text = device.label || t("Camera %0").replace("%0", cameraSelect.length);
|
|
2647
|
+
option.text = device.label || t("settings.cameraSource.numberedCamera", { defaultValue: "Camera %0" }).replace("%0", cameraSelect.length);
|
|
2603
2648
|
cameraSelect.appendChild(option);
|
|
2604
2649
|
if (device.deviceId === s.cameraDeviceId) {
|
|
2605
2650
|
matchingDeviceId = device.deviceId;
|
|
@@ -2617,7 +2662,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2617
2662
|
const option = document.createElement("option");
|
|
2618
2663
|
option.value = s.cameraDeviceId;
|
|
2619
2664
|
const knownInfo = knownCameras[s.cameraDeviceId];
|
|
2620
|
-
option.text = knownInfo ? `${knownInfo.name} (${t("Unavailable")})` : t("Unavailable camera");
|
|
2665
|
+
option.text = knownInfo ? `${knownInfo.name} (${t("settings.cameraSource.unavailableCameraAdjective", { defaultValue: "Unavailable" })})` : t("settings.cameraSource.unavailableCamera", { defaultValue: "Unavailable camera" });
|
|
2621
2666
|
cameraSelect.appendChild(option);
|
|
2622
2667
|
cameraSelect.value = s.cameraDeviceId;
|
|
2623
2668
|
} else {
|
|
@@ -2679,6 +2724,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2679
2724
|
updateStartStopButton();
|
|
2680
2725
|
};
|
|
2681
2726
|
|
|
2727
|
+
let showedCameraError = false;
|
|
2682
2728
|
useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
|
|
2683
2729
|
// Phases:
|
|
2684
2730
|
// 1. "tryPreferredCamera"
|
|
@@ -2803,7 +2849,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2803
2849
|
}
|
|
2804
2850
|
if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
|
|
2805
2851
|
// required track is missing
|
|
2806
|
-
errorMessage.textContent = t("No camera found. Please make sure you have a camera connected and enabled.");
|
|
2852
|
+
errorMessage.textContent = t("video.errors.noCameraFound", { defaultValue: "No camera found. Please make sure you have a camera connected and enabled." });
|
|
2807
2853
|
} else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
|
|
2808
2854
|
// webcam is already in use
|
|
2809
2855
|
// or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
|
|
@@ -2811,7 +2857,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2811
2857
|
// (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
|
|
2812
2858
|
// though care should be given to make it clear it's a list with one item, with something like "(no more cameras detected)" following the list
|
|
2813
2859
|
// or "1 camera source detected" preceding it)
|
|
2814
|
-
errorMessage.textContent = t("Webcam is already in use. Please make sure you have no other programs using the camera.");
|
|
2860
|
+
errorMessage.textContent = t("video.errors.cameraInUse", { defaultValue: "Webcam is already in use. Please make sure you have no other programs using the camera." });
|
|
2815
2861
|
} else if (error.name === "AbortError") {
|
|
2816
2862
|
// webcam is likely already in use
|
|
2817
2863
|
// I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
|
|
@@ -2819,7 +2865,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2819
2865
|
// Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
|
|
2820
2866
|
// I really hope that isn't the problem.
|
|
2821
2867
|
// errorMessage.textContent = "Webcam may already be in use. Please make sure you have no other programs using the camera.";
|
|
2822
|
-
errorMessage.textContent = t("Please make sure no other programs are using the camera and try again.");
|
|
2868
|
+
errorMessage.textContent = t("video.errors.retryAfterClosingOtherPrograms", { defaultValue: "Please make sure no other programs are using the camera and try again." });
|
|
2823
2869
|
// A more honest/helpful message might be:
|
|
2824
2870
|
// errorMessage.textContent = "Please try again and then make sure no other programs are using the camera and try again again.";
|
|
2825
2871
|
// errorMessage.textContent = "Please try again before/after making sure no other programs are using the camera.";
|
|
@@ -2837,25 +2883,32 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
2837
2883
|
// errorMessage.textContent = "The previously selected camera is not available. Please mess around with Video > Camera source.";
|
|
2838
2884
|
// errorMessage.textContent = "The previously selected camera is not available. Try changing Video > Camera source.";
|
|
2839
2885
|
// errorMessage.textContent = "The previously selected camera is not available. Please select a camera from the \"Camera source\" dropdown in the Video settings and if it doesn't show up, it might after you select Default.";
|
|
2840
|
-
errorMessage.textContent = t("The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to.");
|
|
2886
|
+
errorMessage.textContent = t("video.errors.previouslySelectedUnavailable", { defaultValue: "The previously selected camera is not available. Try selecting \"Default\" for Video > Camera source, and then select a specific camera if you need to." });
|
|
2841
2887
|
// It's awkward but that's my best attempt at conveying how you may need to proceed
|
|
2842
2888
|
// without complicated description of how/why the dropdown might be populated with
|
|
2843
2889
|
// fake information until a camera stream is successfully opened.
|
|
2844
2890
|
} else {
|
|
2845
|
-
errorMessage.textContent = t("Webcam does not support the required resolution. Please change your settings.");
|
|
2891
|
+
errorMessage.textContent = t("video.errors.unsupportedResolution", { defaultValue: "Webcam does not support the required resolution. Please change your settings." });
|
|
2846
2892
|
}
|
|
2847
2893
|
} else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
|
|
2848
2894
|
// permission denied in browser
|
|
2849
|
-
errorMessage.textContent = t("Permission denied. Please enable access to the camera.");
|
|
2895
|
+
errorMessage.textContent = t("video.errors.permissionDenied", { defaultValue: "Permission denied. Please enable access to the camera." });
|
|
2850
2896
|
} else if (error.name == "TypeError") {
|
|
2851
2897
|
// empty constraints object
|
|
2852
|
-
errorMessage.textContent = `${t("Something went wrong accessing the camera.")} (${error.name}: ${error.message})`;
|
|
2898
|
+
errorMessage.textContent = `${t("video.errors.accessFailed", { defaultValue: "Something went wrong accessing the camera." })} (${error.name}: ${error.message})`;
|
|
2853
2899
|
} else {
|
|
2854
2900
|
// other errors
|
|
2855
|
-
errorMessage.textContent = `${t("Something went wrong accessing the camera. Please try again.")} (${error.name}: ${error.message})`;
|
|
2901
|
+
errorMessage.textContent = `${t("video.errors.accessFailedRetry", { defaultValue: "Something went wrong accessing the camera. Please try again." })} (${error.name}: ${error.message})`;
|
|
2856
2902
|
}
|
|
2857
|
-
errorMessage.textContent = `${t("⚠️
|
|
2903
|
+
errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${errorMessage.textContent}`;
|
|
2858
2904
|
errorMessage.hidden = false;
|
|
2905
|
+
// Play CSS animation only on retries
|
|
2906
|
+
errorMessage.style.animation = "none";
|
|
2907
|
+
if (showedCameraError) {
|
|
2908
|
+
void errorMessage.offsetWidth; // trigger reflow to allow restarting animation
|
|
2909
|
+
errorMessage.style.animation = "";
|
|
2910
|
+
}
|
|
2911
|
+
showedCameraError = true;
|
|
2859
2912
|
});
|
|
2860
2913
|
};
|
|
2861
2914
|
useDemoFootageButton.onclick = TrackyMouse.useDemoFootage = () => {
|
|
@@ -3562,13 +3615,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3562
3615
|
}
|
|
3563
3616
|
}
|
|
3564
3617
|
|
|
3565
|
-
// TODO: implement these clicking modes for the web library version
|
|
3566
|
-
// and unhide the "Clicking mode" setting in the UI
|
|
3567
|
-
// https://github.com/1j01/tracky-mouse/issues/72
|
|
3568
3618
|
const buttonNames = ["left", "middle", "right"];
|
|
3569
3619
|
for (let buttonIndex = 0; buttonIndex < 3; buttonIndex++) {
|
|
3570
3620
|
if ((clickButton === buttonIndex) !== buttonStates[buttonNames[buttonIndex]]) {
|
|
3571
|
-
|
|
3621
|
+
setMouseButtonState(buttonIndex, clickButton === buttonIndex);
|
|
3572
3622
|
buttonStates[buttonNames[buttonIndex]] = clickButton === buttonIndex;
|
|
3573
3623
|
if ((clickButton === buttonIndex)) {
|
|
3574
3624
|
lastMouseDownTime = performance.now();
|
|
@@ -3589,13 +3639,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3589
3639
|
pointTracker.update(imageData);
|
|
3590
3640
|
}
|
|
3591
3641
|
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3642
|
+
updateInputFeedback?.({
|
|
3643
|
+
headNotFound: !face && !facemeshPrediction,
|
|
3644
|
+
blinkInfo,
|
|
3645
|
+
mouthInfo,
|
|
3646
|
+
});
|
|
3599
3647
|
|
|
3600
3648
|
if (facemeshPrediction) {
|
|
3601
3649
|
ctx.fillStyle = "red";
|
|
@@ -3640,17 +3688,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3640
3688
|
const textYStart = -10;
|
|
3641
3689
|
|
|
3642
3690
|
|
|
3643
|
-
const
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
);
|
|
3652
|
-
const
|
|
3653
|
-
const
|
|
3691
|
+
const headTiltRows = [
|
|
3692
|
+
{ label: t("debug.headTilt.pitch", { defaultValue: "Pitch:" }), value: `${(headTilt.pitch * 180 / Math.PI).toFixed(1)}°` },
|
|
3693
|
+
{ label: t("debug.headTilt.yaw", { defaultValue: "Yaw:" }), value: `${(headTilt.yaw * 180 / Math.PI).toFixed(1)}°` },
|
|
3694
|
+
{ label: t("debug.headTilt.roll", { defaultValue: "Roll:" }), value: `${(headTilt.roll * 180 / Math.PI).toFixed(1)}°` },
|
|
3695
|
+
];
|
|
3696
|
+
const labelWidths = headTiltRows.map(row => ctx.measureText(row.label).width);
|
|
3697
|
+
const maxLabelWidth = Math.max(...labelWidths);
|
|
3698
|
+
const valueColumnTemplate = "-180.0°";
|
|
3699
|
+
const maxValueWidth = ctx.measureText(valueColumnTemplate).width;
|
|
3700
|
+
const labelToValueGap = 10;
|
|
3701
|
+
const boxPadding = 10;
|
|
3702
|
+
const boxWidth = boxPadding * 2 + maxLabelWidth + labelToValueGap + maxValueWidth;
|
|
3703
|
+
const boxHeight = textLineHeight * headTiltRows.length;
|
|
3704
|
+
const valueColumnRightOffset = boxPadding + maxLabelWidth + labelToValueGap + maxValueWidth;
|
|
3654
3705
|
|
|
3655
3706
|
// Calculate screen coordinates for the text box
|
|
3656
3707
|
let screenX = s.mirror ? canvas.width - cx : cx;
|
|
@@ -3661,7 +3712,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3661
3712
|
let textScreenY = screenY + textYStart;
|
|
3662
3713
|
|
|
3663
3714
|
// Clamp to canvas bounds
|
|
3664
|
-
textScreenX = Math.max(
|
|
3715
|
+
textScreenX = Math.max(boxPadding, Math.min(canvas.width - boxWidth - boxPadding, textScreenX));
|
|
3665
3716
|
textScreenY = Math.max(textLineHeight, Math.min(canvas.height - boxHeight + textLineHeight, textScreenY));
|
|
3666
3717
|
|
|
3667
3718
|
ctx.save();
|
|
@@ -3675,12 +3726,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
3675
3726
|
const dx = textScreenX - screenNoseX;
|
|
3676
3727
|
const dy = textScreenY - screenNoseY;
|
|
3677
3728
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3729
|
+
for (let i = 0; i < headTiltRows.length; i++) {
|
|
3730
|
+
const row = headTiltRows[i];
|
|
3731
|
+
const baselineY = dy + textLineHeight * (i + 1);
|
|
3732
|
+
const labelX = dx + boxPadding;
|
|
3733
|
+
const valueTextWidth = ctx.measureText(row.value).width;
|
|
3734
|
+
const valueRightX = dx + valueColumnRightOffset;
|
|
3735
|
+
const valueX = valueRightX - valueTextWidth;
|
|
3736
|
+
ctx.strokeText(row.label, labelX, baselineY);
|
|
3737
|
+
ctx.fillText(row.label, labelX, baselineY);
|
|
3738
|
+
ctx.strokeText(row.value, valueX, baselineY);
|
|
3739
|
+
ctx.fillText(row.value, valueX, baselineY);
|
|
3740
|
+
}
|
|
3684
3741
|
|
|
3685
3742
|
ctx.restore();
|
|
3686
3743
|
|
|
@@ -4013,8 +4070,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4013
4070
|
pointerEl.style.display = "none";
|
|
4014
4071
|
} else {
|
|
4015
4072
|
pointerEl.style.display = "";
|
|
4016
|
-
pointerEl.style.left = `${mouseX}px`;
|
|
4017
|
-
pointerEl.style.top = `${mouseY}px`;
|
|
4073
|
+
pointerEl.style.left = `${Math.floor(mouseX)}px`;
|
|
4074
|
+
pointerEl.style.top = `${Math.floor(mouseY)}px`;
|
|
4018
4075
|
}
|
|
4019
4076
|
if (TrackyMouse.onPointerMove) {
|
|
4020
4077
|
TrackyMouse.onPointerMove(mouseX, mouseY);
|
|
@@ -4030,9 +4087,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4030
4087
|
ctx.lineWidth = 3;
|
|
4031
4088
|
ctx.font = "20px sans-serif";
|
|
4032
4089
|
ctx.beginPath();
|
|
4033
|
-
const text3 = t("Face convergence score:
|
|
4034
|
-
const text1 = t("Face tracking score:
|
|
4035
|
-
const text2 = t("Points based on score:
|
|
4090
|
+
const text3 = `${t("debug.faceConvergenceScore", { defaultValue: "Face convergence score:" })} ${((useFacemesh && facemeshPrediction) ? t("common.notApplicable", { defaultValue: "N/A" }) : faceConvergence.toFixed(4))}`;
|
|
4091
|
+
const text1 = `${t("debug.faceTrackingScore", { defaultValue: "Face tracking score:" })} ${((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4)}`;
|
|
4092
|
+
const text2 = `${t("debug.pointsBasedOnScore", { defaultValue: "Points based on score:" })} ${((useFacemesh && facemeshPrediction) ? pointsBasedOnFaceInViewConfidence : pointsBasedOnFaceScore).toFixed(4)}`;
|
|
4036
4093
|
ctx.strokeText(text1, 50, 50);
|
|
4037
4094
|
ctx.fillText(text1, 50, 50);
|
|
4038
4095
|
ctx.strokeText(text2, 50, 70);
|
|
@@ -4068,14 +4125,26 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4068
4125
|
TrackyMouse.useDemoFootage();
|
|
4069
4126
|
} else if (window.electronAPI) {
|
|
4070
4127
|
TrackyMouse.useCamera();
|
|
4128
|
+
} else {
|
|
4129
|
+
// Passively querying the camera permission isn't supported in all browsers,
|
|
4130
|
+
// hence some of the complex logic in useCamera, but when it is,
|
|
4131
|
+
// we can connect to the camera right away if the permission is already granted.
|
|
4132
|
+
// This speeds up the development cycle, at the very least.
|
|
4133
|
+
navigator.permissions?.query?.({ name: "camera" }).then((status) => {
|
|
4134
|
+
if (status.state === "granted") {
|
|
4135
|
+
TrackyMouse.useCamera();
|
|
4136
|
+
}
|
|
4137
|
+
}, (error) => {
|
|
4138
|
+
console.log("Error querying permissions:", error);
|
|
4139
|
+
});
|
|
4071
4140
|
}
|
|
4072
4141
|
|
|
4073
4142
|
const updateStartStopButton = () => {
|
|
4074
4143
|
if (paused) {
|
|
4075
|
-
startStopButton.textContent = t("Start");
|
|
4144
|
+
startStopButton.textContent = t("ui.startStopButton.start", { defaultValue: "Start" });
|
|
4076
4145
|
startStopButton.setAttribute("aria-pressed", "false");
|
|
4077
4146
|
} else {
|
|
4078
|
-
startStopButton.textContent = t("Stop");
|
|
4147
|
+
startStopButton.textContent = t("ui.startStopButton.stop", { defaultValue: "Stop" });
|
|
4079
4148
|
startStopButton.setAttribute("aria-pressed", "true");
|
|
4080
4149
|
}
|
|
4081
4150
|
};
|
|
@@ -4085,9 +4154,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
|
|
|
4085
4154
|
pointerEl.style.display = "none";
|
|
4086
4155
|
}
|
|
4087
4156
|
updateStartStopButton();
|
|
4088
|
-
|
|
4089
|
-
window.electronAPI.notifyToggleState(!paused);
|
|
4090
|
-
}
|
|
4157
|
+
notifyToggleState?.(!paused);
|
|
4091
4158
|
};
|
|
4092
4159
|
const handleShortcut = (shortcutType) => {
|
|
4093
4160
|
if (shortcutType === "toggle-tracking") {
|
|
@@ -4229,15 +4296,17 @@ TrackyMouse.init = function (div, opts = {}) {
|
|
|
4229
4296
|
TrackyMouse.initScreenOverlay = () => {
|
|
4230
4297
|
|
|
4231
4298
|
const template = `
|
|
4299
|
+
<div class="tracky-mouse-hide-near-cursor">
|
|
4232
4300
|
<div class="tracky-mouse-absolute-center">
|
|
4233
4301
|
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
|
|
4234
|
-
<img src="
|
|
4302
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
|
|
4235
4303
|
</div>
|
|
4236
4304
|
<div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
|
|
4237
|
-
<img src="
|
|
4305
|
+
<img src="${TrackyMouse.dependenciesRoot}/images/head-not-found.svg" alt="head not found" width="128" height="128">
|
|
4238
4306
|
</div>
|
|
4239
4307
|
</div>
|
|
4240
4308
|
<div id="tracky-mouse-screen-overlay-message"></div>
|
|
4309
|
+
</div>
|
|
4241
4310
|
`;
|
|
4242
4311
|
const fragment = document.createRange().createContextualFragment(template);
|
|
4243
4312
|
document.body.appendChild(fragment);
|
|
@@ -4245,8 +4314,11 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4245
4314
|
const message = document.getElementById("tracky-mouse-screen-overlay-message");
|
|
4246
4315
|
message.dir = "auto";
|
|
4247
4316
|
|
|
4317
|
+
const hideNearCursorEls = document.querySelectorAll(".tracky-mouse-hide-near-cursor");
|
|
4318
|
+
|
|
4248
4319
|
const inputFeedbackCanvas = document.createElement("canvas");
|
|
4249
|
-
inputFeedbackCanvas.style.position = "
|
|
4320
|
+
inputFeedbackCanvas.style.position = "fixed";
|
|
4321
|
+
inputFeedbackCanvas.style.zIndex = "899990"; // just below .tracky-mouse-pointer
|
|
4250
4322
|
inputFeedbackCanvas.style.top = "0";
|
|
4251
4323
|
inputFeedbackCanvas.style.left = "0";
|
|
4252
4324
|
inputFeedbackCanvas.style.pointerEvents = "none";
|
|
@@ -4282,10 +4354,11 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4282
4354
|
// inputFeedbackCanvas.style.transform = `translate(${x - inputFeedbackCanvas.width / 2}px, ${y - inputFeedbackCanvas.height / 2}px)`;
|
|
4283
4355
|
// inputFeedbackCanvas.style.transform = `translate(${x}px, ${y}px)`;
|
|
4284
4356
|
inputFeedbackCanvas.style.transform = `translate(${Math.min(x, window.innerWidth - inputFeedbackCanvas.width)}px, ${Math.min(y, window.innerHeight - inputFeedbackCanvas.height)}px)`;
|
|
4357
|
+
|
|
4285
4358
|
}
|
|
4286
4359
|
|
|
4287
4360
|
function update(data) {
|
|
4288
|
-
const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset } = data;
|
|
4361
|
+
const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset, systemMousePosition } = data;
|
|
4289
4362
|
|
|
4290
4363
|
message.style.bottom = `${bottomOffset}px`;
|
|
4291
4364
|
|
|
@@ -4294,21 +4367,31 @@ TrackyMouse.initScreenOverlay = () => {
|
|
|
4294
4367
|
// - bad lighting conditions
|
|
4295
4368
|
// see: https://github.com/1j01/tracky-mouse/issues/26
|
|
4296
4369
|
|
|
4297
|
-
document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback);
|
|
4298
|
-
document.body.classList.toggle("tracky-mouse-head-not-found", inputFeedback.headNotFound);
|
|
4370
|
+
document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback ?? false);
|
|
4371
|
+
document.body.classList.toggle("tracky-mouse-head-not-found", inputFeedback.headNotFound ?? false);
|
|
4299
4372
|
|
|
4300
4373
|
message.innerText = messageText;
|
|
4301
4374
|
|
|
4302
4375
|
if (!isEnabled && !isManualTakeback) {
|
|
4303
4376
|
// Fade out the message after a little while so it doesn't get in the way.
|
|
4304
4377
|
// TODO: make sure animation isn't interrupted by inputFeedback updates.
|
|
4305
|
-
message.style.animation = "tracky-mouse-
|
|
4378
|
+
message.style.animation = "tracky-mouse-fade-out 2s ease-in-out forwards 10s";
|
|
4306
4379
|
} else {
|
|
4307
4380
|
message.style.animation = "";
|
|
4308
4381
|
message.style.opacity = "1";
|
|
4309
4382
|
}
|
|
4310
4383
|
|
|
4311
4384
|
drawInputFeedback(data);
|
|
4385
|
+
|
|
4386
|
+
if (systemMousePosition) {
|
|
4387
|
+
const { x, y } = systemMousePosition;
|
|
4388
|
+
// TODO: optimize CSS parsing by using CSS variables?
|
|
4389
|
+
const maskImage = `radial-gradient(circle at ${x}px ${y}px, transparent 0, transparent 50px, rgba(0, 0, 0, 0.85) 140px, rgba(0, 0, 0, 1) 200px, rgba(0, 0, 0, 1) 100%)`;
|
|
4390
|
+
for (const el of hideNearCursorEls) {
|
|
4391
|
+
el.style.webkitMaskImage = maskImage;
|
|
4392
|
+
el.style.maskImage = maskImage;
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4312
4395
|
}
|
|
4313
4396
|
|
|
4314
4397
|
return {
|