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.
Files changed (65) hide show
  1. package/images/head-not-found.svg +135 -0
  2. package/images/manual-takeback.svg +127 -0
  3. package/locales/ar/translation.json +197 -202
  4. package/locales/ar-EG/translation.json +197 -202
  5. package/locales/bg/translation.json +197 -202
  6. package/locales/bn/translation.json +197 -202
  7. package/locales/ca/translation.json +197 -202
  8. package/locales/ce/translation.json +197 -202
  9. package/locales/ceb/translation.json +197 -202
  10. package/locales/cs/translation.json +197 -202
  11. package/locales/da/translation.json +197 -202
  12. package/locales/de/translation.json +197 -202
  13. package/locales/el/translation.json +197 -202
  14. package/locales/emoji/translation.json +197 -202
  15. package/locales/en/translation.json +197 -202
  16. package/locales/eo/translation.json +197 -202
  17. package/locales/es/translation.json +197 -202
  18. package/locales/eu/translation.json +197 -202
  19. package/locales/fa/translation.json +197 -202
  20. package/locales/fi/translation.json +197 -202
  21. package/locales/fr/translation.json +197 -202
  22. package/locales/gu/translation.json +197 -202
  23. package/locales/ha/translation.json +197 -202
  24. package/locales/he/translation.json +197 -202
  25. package/locales/hi/translation.json +197 -202
  26. package/locales/hr/translation.json +197 -202
  27. package/locales/hu/translation.json +197 -202
  28. package/locales/hy/translation.json +197 -202
  29. package/locales/id/translation.json +197 -202
  30. package/locales/it/translation.json +197 -202
  31. package/locales/ja/translation.json +197 -202
  32. package/locales/jv/translation.json +197 -202
  33. package/locales/ko/translation.json +197 -202
  34. package/locales/mr/translation.json +197 -202
  35. package/locales/ms/translation.json +197 -202
  36. package/locales/nan/translation.json +197 -202
  37. package/locales/nb/translation.json +197 -202
  38. package/locales/nl/translation.json +197 -202
  39. package/locales/pa/translation.json +197 -202
  40. package/locales/pl/translation.json +197 -202
  41. package/locales/pt/translation.json +197 -202
  42. package/locales/pt-BR/translation.json +197 -202
  43. package/locales/ro/translation.json +197 -202
  44. package/locales/ru/translation.json +197 -202
  45. package/locales/sk/translation.json +197 -202
  46. package/locales/sl/translation.json +197 -202
  47. package/locales/sr/translation.json +197 -202
  48. package/locales/sv/translation.json +197 -202
  49. package/locales/sw/translation.json +197 -202
  50. package/locales/ta/translation.json +197 -202
  51. package/locales/te/translation.json +197 -202
  52. package/locales/th/translation.json +197 -202
  53. package/locales/tl/translation.json +197 -202
  54. package/locales/tr/translation.json +197 -202
  55. package/locales/tt/translation.json +197 -202
  56. package/locales/uk/translation.json +197 -202
  57. package/locales/ur/translation.json +197 -202
  58. package/locales/uz/translation.json +197 -202
  59. package/locales/vi/translation.json +197 -202
  60. package/locales/war/translation.json +197 -202
  61. package/locales/zh/translation.json +197 -202
  62. package/locales/zh-simplified/translation.json +198 -203
  63. package/package.json +2 -1
  64. package/tracky-mouse.css +72 -6
  65. 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 = (s) => s;
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("config.click must be a function"));
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("config.shouldDrag must be a function"));
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("config.noCenter must be a function"));
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("config.isEquivalentTarget must be a function"));
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("config.dwellClickEvenIfPaused must be a function"));
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("config.beforeDispatch must be a function"));
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("config.afterDispatch must be a function"));
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("config.beforePointerDownDispatch must be a function"));
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("config.isHeld must be a function"));
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("config.retarget[%0].withinMargin must be a number").replace("%0", i));
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, { statsJs = false }, reinit) {
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 = (s) => translations[s] ?? s;
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(`Blends between using point tracking (2D) and detected head tilt (3D).
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(`Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
1751
- 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.`),
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(`Adjusts the position of the cursor when the camera sees the head facing straight ahead.
1823
- ⚠️ This horizontal offset is not recommended. Move the camera instead if possible. 📷`),
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(`Switches the left and right mouse buttons.
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("Delay before dragging&nbsp;&nbsp;&nbsp;"), // TODO: avoid non-breaking space hack
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
- // description: t(`Prevents mouse movement for the specified time after a click starts.
1920
- // 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.`),
1921
- description: t(`Locks mouse movement for the given duration during the start of a click.
1922
- 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.`),
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:\n") + t("Failed to parse known cameras from localStorage:\n") + error.message);
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:\n") + result.error);
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:\n") + error.message);
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 += t("\n\nOptions:\n") + setting.options.map(option => `• ${option.label}${option.description ? `: ${option.description}` : ''}`).join("\n");
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
- // Desktop app: start from any saved settings in the main process,
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
- deserializeSettings(JSON.parse(localStorage.getItem("tracky-mouse-settings")), initialLoad);
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("⚠️ ")}${errorMessage.textContent}`;
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
- window.electronAPI?.setMouseButtonState(buttonIndex, clickButton === buttonIndex);
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
- if (window.electronAPI) {
3593
- window.electronAPI.updateInputFeedback({
3594
- headNotFound: !face && !facemeshPrediction,
3595
- blinkInfo,
3596
- mouthInfo,
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 pitchText = t("Pitch: ") + `${(headTilt.pitch * 180 / Math.PI).toFixed(1)}°`;
3644
- const yawText = t("Yaw: ") + `${(headTilt.yaw * 180 / Math.PI).toFixed(1)}°`;
3645
- const rollText = t("Roll: ") + `${(headTilt.roll * 180 / Math.PI).toFixed(1)}°`;
3646
-
3647
- const boxWidth = Math.max(
3648
- ctx.measureText(pitchText).width,
3649
- ctx.measureText(yawText).width,
3650
- ctx.measureText(rollText).width
3651
- );
3652
- const boxHeight = textLineHeight * 3;
3653
- const padding = 10;
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(padding, Math.min(canvas.width - boxWidth - padding, textScreenX));
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
- ctx.strokeText(pitchText, dx, dy);
3679
- ctx.fillText(pitchText, dx, dy);
3680
- ctx.strokeText(yawText, dx, dy + textLineHeight);
3681
- ctx.fillText(yawText, dx, dy + textLineHeight);
3682
- ctx.strokeText(rollText, dx, dy + textLineHeight * 2);
3683
- ctx.fillText(rollText, dx, dy + textLineHeight * 2);
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: ") + ((useFacemesh && facemeshPrediction) ? t("N/A") : faceConvergence.toFixed(4));
4034
- const text1 = t("Face tracking score: ") + ((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4);
4035
- const text2 = t("Points based on score: ") + ((useFacemesh && facemeshPrediction) ? pointsBasedOnFaceInViewConfidence : pointsBasedOnFaceScore).toFixed(4);
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
- if (window.electronAPI) {
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="../images/manual-takeback.svg" alt="hand reaching for mouse" width="128" height="128">
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="../images/head-not-found.svg" alt="head not found" width="128" height="128">
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 = "absolute";
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-screen-overlay-message-fade-out 2s ease-in-out forwards 10s";
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 {