tracky-mouse 2.4.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 (66) 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 +199 -0
  5. package/locales/bg/translation.json +199 -0
  6. package/locales/bn/translation.json +197 -202
  7. package/locales/ca/translation.json +199 -0
  8. package/locales/ce/translation.json +199 -0
  9. package/locales/ceb/translation.json +199 -0
  10. package/locales/cs/translation.json +199 -0
  11. package/locales/da/translation.json +199 -0
  12. package/locales/de/translation.json +197 -202
  13. package/locales/el/translation.json +199 -0
  14. package/locales/emoji/emoji-translation-notes.md +147 -0
  15. package/locales/emoji/translation.json +199 -0
  16. package/locales/en/translation.json +197 -202
  17. package/locales/eo/translation.json +199 -0
  18. package/locales/es/translation.json +197 -202
  19. package/locales/eu/translation.json +199 -0
  20. package/locales/fa/translation.json +199 -0
  21. package/locales/fi/translation.json +199 -0
  22. package/locales/fr/translation.json +197 -202
  23. package/locales/gu/translation.json +199 -0
  24. package/locales/ha/translation.json +199 -0
  25. package/locales/he/translation.json +199 -0
  26. package/locales/hi/translation.json +197 -202
  27. package/locales/hr/translation.json +199 -0
  28. package/locales/hu/translation.json +199 -0
  29. package/locales/hy/translation.json +199 -0
  30. package/locales/id/translation.json +199 -0
  31. package/locales/it/translation.json +197 -202
  32. package/locales/ja/translation.json +197 -202
  33. package/locales/jv/translation.json +199 -0
  34. package/locales/ko/translation.json +197 -202
  35. package/locales/mr/translation.json +199 -0
  36. package/locales/ms/translation.json +199 -0
  37. package/locales/nan/translation.json +199 -0
  38. package/locales/nb/translation.json +199 -0
  39. package/locales/nl/translation.json +197 -202
  40. package/locales/pa/translation.json +199 -0
  41. package/locales/pl/translation.json +199 -0
  42. package/locales/pt/translation.json +199 -0
  43. package/locales/pt-BR/translation.json +199 -0
  44. package/locales/ro/translation.json +199 -0
  45. package/locales/ru/translation.json +199 -0
  46. package/locales/sk/translation.json +199 -0
  47. package/locales/sl/translation.json +199 -0
  48. package/locales/sr/translation.json +199 -0
  49. package/locales/sv/translation.json +199 -0
  50. package/locales/sw/translation.json +199 -0
  51. package/locales/ta/translation.json +199 -0
  52. package/locales/te/translation.json +199 -0
  53. package/locales/th/translation.json +199 -0
  54. package/locales/tl/translation.json +199 -0
  55. package/locales/tr/translation.json +199 -0
  56. package/locales/tt/translation.json +199 -0
  57. package/locales/uk/translation.json +199 -0
  58. package/locales/ur/translation.json +199 -0
  59. package/locales/uz/translation.json +199 -0
  60. package/locales/vi/translation.json +199 -0
  61. package/locales/war/translation.json +199 -0
  62. package/locales/zh/translation.json +197 -202
  63. package/locales/zh-simplified/translation.json +199 -0
  64. package/package.json +2 -1
  65. package/tracky-mouse.css +72 -6
  66. package/tracky-mouse.js +327 -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
 
@@ -584,7 +601,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
584
601
  }
585
602
  const availableLanguages = [
586
603
  // GENERATED by scripts/update-locales.js
587
- "ar", "bn", "de", "en", "es", "fr", "hi", "it", "ja", "ko", "nl", "zh"
604
+ "ar", "ar-EG", "bg", "bn", "ca", "ce", "ceb", "cs", "da", "de", "el", "emoji", "en", "eo", "es", "eu", "fa", "fi", "fr", "gu", "ha", "he", "hi", "hr", "hu", "hy", "id", "it", "ja", "jv", "ko", "mr", "ms", "nan", "nb", "nl", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sl", "sr", "sv", "sw", "ta", "te", "th", "tl", "tr", "tt", "uk", "ur", "uz", "vi", "war", "zh", "zh-simplified"
588
605
  // END GENERATED
589
606
  ];
590
607
  // Fallback to a valid dropdown value for unsupported locales
@@ -616,10 +633,12 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
616
633
  console.warn("Could not load translations for TrackyMouse UI:", e);
617
634
  }
618
635
  const rtlLanguages = ["ar", "he", "fa", "ur"]; // Right-to-left languages (current and future)
619
- const isRTL = rtlLanguages.includes(locale);
620
- const t = (s) => translations[s] ?? s;
636
+ const isRTL = rtlLanguages.includes(locale.split("-")[0]);
637
+ const t = (key, options = {}) => translations[key] ?? options.defaultValue ?? key;
621
638
  // console.trace("Initializing UI with locale", locale);
622
639
 
640
+ // language name mappings marked with * may not be ISO 639-1
641
+ // they may be ISO 639-3 or bespoke
623
642
  // spell-checker:disable
624
643
  const languageNames = {
625
644
  // "639-1": [["ISO language name"], ["Native name (endonym)"]],
@@ -630,6 +649,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
630
649
  sq: [["Albanian"], ["Shqip"]],
631
650
  am: [["Amharic"], ["አማርኛ"]],
632
651
  ar: [["Arabic"], ["العربية"]],
652
+ "ar-EG": [["Egyptian Arabic"], ["العربية المصرية"]],//*
633
653
  an: [["Aragonese"], ["Aragonés"]],
634
654
  hy: [["Armenian"], ["Հայերեն"]],
635
655
  as: [["Assamese"], ["অসমীয়া"]],
@@ -639,6 +659,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
639
659
  az: [["Azerbaijani"], ["Azərbaycan Dili"]],
640
660
  bm: [["Bambara"], ["Bamanankan"]],
641
661
  ba: [["Bashkir"], ["Башҡорт Теле"]],
662
+ emoji: [["Emoji"], ["😃📝"]],//*
642
663
  eu: [["Basque"], ["Euskara", "Euskera"]],
643
664
  be: [["Belarusian"], ["Беларуская Мова"]],
644
665
  bn: [["Bengali"], ["বাংলা"]],
@@ -651,6 +672,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
651
672
  ca: [["Catalan", "Valencian"], ["Català", "Valencià"]],
652
673
  ch: [["Chamorro"], ["Chamoru"]],
653
674
  ce: [["Chechen"], ["Нохчийн Мотт"]],
675
+ ceb: [["Cebuano"], ["Bisayâ", "Binisayâ"]],//*
654
676
  ny: [["Chichewa", "Chewa", "Nyanja"], ["ChiCheŵa", "Chinyanja"]],
655
677
  // zh: [["Chinese"], ["中文", "Zhōngwén", "汉语", "漢語"]],
656
678
  // The ISO 639-1 code "zh" doesn't refer to Traditional Chinese specifically,
@@ -658,8 +680,8 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
658
680
  // so this is overly specific for now.
659
681
  // @TODO: do this cleaner by establishing a mapping between ISO codes (such as "zh") and default language IDs (such as "zh-traditional")
660
682
  zh: [["Traditional Chinese"], ["繁體中文", "傳統中文", "正體中文", "繁体中文"]],
661
- "zh-traditional": [["Traditional Chinese"], ["繁體中文", "傳統中文", "正體中文", "繁体中文"]], // made-up ID, not real ISO 639-1
662
- "zh-simplified": [["Simplified Chinese"], ["简体中文"]], // made-up ID, not real ISO 639-1
683
+ "zh-traditional": [["Traditional Chinese"], ["繁體中文", "傳統中文", "正體中文", "繁体中文"]], //*
684
+ "zh-simplified": [["Simplified Chinese"], ["简体中文"]], //*
663
685
  cv: [["Chuvash"], ["Чӑваш Чӗлхи"]],
664
686
  kw: [["Cornish"], ["Kernewek"]],
665
687
  co: [["Corsican"], ["Corsu", "Lingua Corsa"]],
@@ -738,6 +760,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
738
760
  mh: [["Marshallese"], ["Kajin M̧ajeļ"]],
739
761
  mn: [["Mongolian"], ["Монгол Хэл"]],
740
762
  na: [["Nauru"], ["Dorerin Naoero"]],
763
+ nan: [["Minnan", "Taiwanese Hokkien"], ["閩南語", "闽南语", "Bàn-lâm-gú", "Bân-lâm-gí"]],//* (technically Hokkien is a branch of Minnan; also idk what names are preferred)
741
764
  nv: [["Navajo", "Navaho"], ["Diné Bizaad"]],
742
765
  nd: [["North Ndebele"], ["IsiNdebele"]],
743
766
  ne: [["Nepali"], ["नेपाली"]],
@@ -759,8 +782,8 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
759
782
  pl: [["Polish"], ["Język Polski", "Polszczyzna"]],
760
783
  ps: [["Pashto", "Pushto"], ["پښتو"]],
761
784
  pt: [["Portuguese"], ["Português"]],
762
- "pt-br": [["Brazilian Portuguese"], ["Português Brasileiro"]],
763
- "pt-pt": [["Portuguese (Portugal)"], ["Português De Portugal"]],
785
+ "pt-BR": [["Brazilian Portuguese"], ["Português Brasileiro"]],
786
+ "pt-PT": [["Portuguese (Portugal)"], ["Português De Portugal"]],
764
787
  qu: [["Quechua"], ["Runa Simi", "Kichwa"]],
765
788
  rm: [["Romansh"], ["Rumantsch Grischun"]],
766
789
  rn: [["Rundi"], ["Ikirundi"]],
@@ -808,6 +831,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
808
831
  vi: [["Vietnamese"], ["Tiếng Việt"]],
809
832
  vo: [["Volapük"], ["Volapük"]],
810
833
  wa: [["Walloon"], ["Walon"]],
834
+ war: [["Waray"], ["Winaray"]],//*
811
835
  cy: [["Welsh"], ["Cymraeg"]],
812
836
  wo: [["Wolof"], ["Wollof"]],
813
837
  fy: [["Western Frisian"], ["Frysk"]],
@@ -1551,7 +1575,19 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1551
1575
  zza: "TR",
1552
1576
  };
1553
1577
 
1554
- function getLanguageEmoji(locale) {
1578
+ function getLanguageFlagEmoji(locale) {
1579
+
1580
+ if (locale === "emoji") {
1581
+ return "🏳️‍🌈";
1582
+ } else if (locale === "eo") {
1583
+ // return "🏴🟩";
1584
+ return "🟩";
1585
+ // return `<svg viewBox="0 0 600 400" height="20">
1586
+ // <path fill="#FFF" d="m0,0h202v202H0"/>
1587
+ // <path fill="#090" d="m0,200H200V0H600V400H0m58-243 41-126 41,126-107-78h133"/>
1588
+ // </svg>`;
1589
+ }
1590
+
1555
1591
  var split = locale.toUpperCase().split(/-|_/);
1556
1592
  var lang = split.shift();
1557
1593
  var code = split.pop();
@@ -1575,21 +1611,21 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1575
1611
  uiContainer.dir = isRTL ? "rtl" : "ltr";
1576
1612
  uiContainer.innerHTML = `
1577
1613
  <div class="tracky-mouse-controls">
1578
- <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>
1579
1615
  </div>
1580
1616
  <div class="tracky-mouse-canvas-container-container">
1581
1617
  <div class="tracky-mouse-canvas-container">
1582
1618
  <div class="tracky-mouse-canvas-overlay">
1583
- <button class="tracky-mouse-use-camera-button">${t("Allow Camera Access")}</button>
1584
- <!--<button class="tracky-mouse-use-camera-button">${t("Use my camera")}</button>-->
1585
- <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>
1586
1622
  <div class="tracky-mouse-error-message" role="alert" hidden></div>
1587
1623
  </div>
1588
1624
  <canvas class="tracky-mouse-canvas"></canvas>
1589
1625
  </div>
1590
1626
  </div>
1591
1627
  <p class="tracky-mouse-desktop-app-download-message">
1592
- ${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.' })}
1593
1629
  </p>
1594
1630
  `;
1595
1631
  if (!div) {
@@ -1612,10 +1648,10 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1612
1648
  const settingsCategories = [
1613
1649
  {
1614
1650
  type: "group",
1615
- label: t("Cursor Movement"),
1651
+ label: t("settings.sections.cursorMovement.label", { defaultValue: "Cursor Movement" }),
1616
1652
  settings: [
1617
1653
  {
1618
- label: t("Tilt influence"),
1654
+ label: t("settings.tiltInfluence.label", { defaultValue: "Tilt influence" }),
1619
1655
  className: "tracky-mouse-tilt-influence",
1620
1656
  key: "headTrackingTiltInfluence",
1621
1657
  settingValueToInputValue: (settingValue) => settingValue * 100,
@@ -1625,20 +1661,21 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1625
1661
  max: 100,
1626
1662
  default: 0,
1627
1663
  labels: {
1628
- // min: t("Optical flow"), // too technical
1629
- // min: t("Point tracking"), // still technical but at least it's terminology we're already using
1630
- min: t("Point tracking (2D)"),
1631
- // max: t("Head tilt"),
1632
- 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)" }),
1633
1669
  },
1634
- // description: t("Determines whether cursor movement is based on 3D head tilt, or 2D motion of the face in the camera feed."),
1635
- 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).
1636
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.
1637
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.
1638
- - 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.` }),
1639
1676
  },
1640
1677
  {
1641
- label: t("Motion threshold"),
1678
+ label: t("settings.motionThreshold.label", { defaultValue: "Motion threshold" }),
1642
1679
  className: "tracky-mouse-min-distance",
1643
1680
  key: "headTrackingMinDistance",
1644
1681
  type: "slider",
@@ -1646,20 +1683,20 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1646
1683
  max: 10,
1647
1684
  default: 0,
1648
1685
  labels: {
1649
- min: t("Free"),
1650
- max: t("Steady"),
1686
+ min: t("settings.motionThreshold.sliderMin", { defaultValue: "Free" }),
1687
+ max: t("settings.motionThreshold.sliderMax", { defaultValue: "Steady" }),
1651
1688
  },
1652
- description: t("Minimum distance to move the cursor in one frame, in pixels. Helps to fully stop the cursor."),
1653
- // description: t("Movement less than this distance in pixels will be ignored."),
1654
- // 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." }),
1655
1692
  },
1656
1693
  {
1657
1694
  type: "group",
1658
- label: t("Point tracking"),
1695
+ label: t("settings.sections.pointTracking.label", { defaultValue: "Point tracking" }),
1659
1696
  disabled: () => s.headTrackingTiltInfluence === 1,
1660
1697
  settings: [
1661
1698
  {
1662
- label: t("Horizontal sensitivity"),
1699
+ label: t("settings.pointTracking.horizontalSensitivity.label", { defaultValue: "Horizontal sensitivity" }),
1663
1700
  className: "tracky-mouse-sensitivity-x",
1664
1701
  key: "headTrackingSensitivityX",
1665
1702
  settingValueToInputValue: (settingValue) => settingValue * 1000,
@@ -1669,13 +1706,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1669
1706
  max: 100,
1670
1707
  default: 25,
1671
1708
  labels: {
1672
- min: t("Slow"),
1673
- max: t("Fast"),
1709
+ min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
1710
+ max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
1674
1711
  },
1675
- 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." }),
1676
1713
  },
1677
1714
  {
1678
- label: t("Vertical sensitivity"),
1715
+ label: t("settings.pointTracking.verticalSensitivity.label", { defaultValue: "Vertical sensitivity" }),
1679
1716
  className: "tracky-mouse-sensitivity-y",
1680
1717
  key: "headTrackingSensitivityY",
1681
1718
  settingValueToInputValue: (settingValue) => settingValue * 1000,
@@ -1685,13 +1722,13 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1685
1722
  max: 100,
1686
1723
  default: 50,
1687
1724
  labels: {
1688
- min: t("Slow"),
1689
- max: t("Fast"),
1725
+ min: t("settings.shared.sliderMinSlow", { defaultValue: "Slow" }),
1726
+ max: t("settings.shared.sliderMaxFast", { defaultValue: "Fast" }),
1690
1727
  },
1691
- 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." }),
1692
1729
  },
1693
1730
  // {
1694
- // label: t("Smoothing"),
1731
+ // label: t("settings.pointTracking.smoothing.label", { defaultValue: "Smoothing" }),
1695
1732
  // className: "tracky-mouse-smoothing",
1696
1733
  // key: "headTrackingSmoothing",
1697
1734
  // type: "slider",
@@ -1699,8 +1736,8 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1699
1736
  // max: 100,
1700
1737
  // default: 50,
1701
1738
  // labels: {
1702
- // min: t("Linear"), // or "Direct", "Raw", "None"
1703
- // 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"
1704
1741
  // },
1705
1742
  // },
1706
1743
 
@@ -1713,7 +1750,7 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1713
1750
  // Should it be swapped? What does other software with acceleration control look like?
1714
1751
  // In Windows it's just a checkbox apparently, but it could go as far as a custom curve editor.
1715
1752
  {
1716
- label: t("Acceleration"),
1753
+ label: t("settings.pointTracking.acceleration.label", { defaultValue: "Acceleration" }),
1717
1754
  className: "tracky-mouse-acceleration",
1718
1755
  key: "headTrackingAcceleration",
1719
1756
  settingValueToInputValue: (settingValue) => settingValue * 100,
@@ -1723,23 +1760,24 @@ TrackyMouse._initInner = function (div, { statsJs = false }, reinit) {
1723
1760
  max: 100,
1724
1761
  default: 50,
1725
1762
  labels: {
1726
- min: t("Linear"), // or "Direct", "Raw"
1727
- max: t("Smooth"),
1763
+ min: t("settings.shared.sliderMinLinear", { defaultValue: "Linear" }), // or "Direct", "Raw"
1764
+ max: t("settings.shared.sliderMaxSmooth", { defaultValue: "Smooth" }),
1728
1765
  },
1729
- // description: t("Higher acceleration makes the cursor move faster when the head moves quickly, and slower when the head moves slowly."),
1730
- // description: t("Makes the cursor move extra fast for quick head movements, and extra slow for slow head movements. Helps to stabilize the cursor."),
1731
- description: t(`Makes the cursor move relatively fast for quick head movements, and relatively slow for slow head movements.
1732
- 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.` }),
1733
1771
  },
1734
1772
  ],
1735
1773
  },
1736
1774
  {
1737
1775
  type: "group",
1738
- label: t("Head tilt calibration"),
1776
+ label: t("settings.sections.headTiltCalibration.label", { defaultValue: "Head tilt calibration" }),
1739
1777
  disabled: () => s.headTrackingTiltInfluence === 0,
1740
1778
  settings: [
1741
1779
  {
1742
- label: t("Horizontal tilt range"),
1780
+ label: t("settings.headTilt.horizontalRange.label", { defaultValue: "Horizontal tilt range" }),
1743
1781
  className: "tracky-mouse-head-tilt-yaw-range",
1744
1782
  key: "headTiltYawRange",
1745
1783
  settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
@@ -1749,16 +1787,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1749
1787
  max: 90,
1750
1788
  default: 60,
1751
1789
  labels: {
1752
- min: t("Little neck movement"),
1753
- 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" }),
1754
1792
  },
1755
- // description: t("Range of horizontal head tilt that moves the cursor from one side of the screen to the other."),
1756
- // description: t("How much you need to tilt your head left and right to reach the edges of the screen."),
1757
- // description: t("How much you need to tilt your head left or right to reach the edge of the screen."),
1758
- 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." }),
1759
1797
  },
1760
1798
  {
1761
- label: t("Vertical tilt range"),
1799
+ label: t("settings.headTilt.verticalRange.label", { defaultValue: "Vertical tilt range" }),
1762
1800
  className: "tracky-mouse-head-tilt-pitch-range",
1763
1801
  key: "headTiltPitchRange",
1764
1802
  settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
@@ -1768,17 +1806,17 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1768
1806
  max: 60,
1769
1807
  default: 25,
1770
1808
  labels: {
1771
- min: t("Little neck movement"),
1772
- 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" }),
1773
1811
  },
1774
- // description: t("Range of vertical head tilt required to move the cursor from the top to the bottom of the screen."),
1775
- // description: t("How much you need to tilt your head up and down to reach the edges of the screen."),
1776
- // description: t("How much you need to tilt your head up or down to reach the edge of the screen."),
1777
- 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." }),
1778
1816
  },
1779
1817
  {
1780
1818
  // label: "Horizontal tilt offset",
1781
- label: t("Horizontal cursor offset"),
1819
+ label: t("settings.headTilt.horizontalOffset.label", { defaultValue: "Horizontal cursor offset" }),
1782
1820
  className: "tracky-mouse-head-tilt-yaw-offset",
1783
1821
  key: "headTiltYawOffset",
1784
1822
  settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
@@ -1788,8 +1826,8 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1788
1826
  max: 45,
1789
1827
  default: 0,
1790
1828
  labels: {
1791
- min: t("Left"),
1792
- max: t("Right"),
1829
+ min: t("settings.shared.directionLeft", { defaultValue: "Left" }),
1830
+ max: t("settings.shared.directionRight", { defaultValue: "Right" }),
1793
1831
  },
1794
1832
  // TODO: how to describe this??
1795
1833
  // Specifically, how to disambiguate which direction is which / which way to adjust it?
@@ -1797,15 +1835,16 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1797
1835
  // Since it's opposite, even though it's technically yaw (angle units), it's easier to think of as moving the cursor.
1798
1836
  // Hence I've renamed the setting.
1799
1837
  // A later update might change the definitions and include a settings file format upgrade step.
1800
- // description: t("Adjusts the center position of horizontal head tilt. Not recommended. Move the camera instead if possible."),
1801
- // 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." }),
1802
1840
  // TODO: should this say "horizontal" in the (main part of the) description?
1803
- description: t(`Adjusts the position of the cursor when the camera sees the head facing straight ahead.
1804
- ⚠️ 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. 📷` }),
1805
1844
  },
1806
1845
  {
1807
1846
  // label: "Vertical tilt offset",
1808
- label: t("Vertical cursor offset"),
1847
+ label: t("settings.headTilt.verticalOffset.label", { defaultValue: "Vertical cursor offset" }),
1809
1848
  className: "tracky-mouse-head-tilt-pitch-offset",
1810
1849
  key: "headTiltPitchOffset",
1811
1850
  settingValueToInputValue: (settingValue) => settingValue * 180 / Math.PI,
@@ -1815,11 +1854,11 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1815
1854
  max: 30,
1816
1855
  default: 2.5,
1817
1856
  labels: {
1818
- min: t("Down"),
1819
- max: t("Up"),
1857
+ min: t("settings.shared.directionDown", { defaultValue: "Down" }),
1858
+ max: t("settings.shared.directionUp", { defaultValue: "Up" }),
1820
1859
  },
1821
- // description: t("Adjusts the center position of vertical head tilt."),
1822
- 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." }),
1823
1862
  },
1824
1863
  ],
1825
1864
  },
@@ -1839,42 +1878,43 @@ Helps to stabilize the cursor. However, when using point tracking in combination
1839
1878
  // which awkwardly affects what mouse button serenade-driver sends; this doesn't affect the web version.
1840
1879
  {
1841
1880
  type: "group",
1842
- label: t("Clicking"),
1881
+ label: t("settings.sections.clicking.label", { defaultValue: "Clicking" }),
1843
1882
  settings: [
1844
1883
  {
1845
- label: t("Clicking mode:"), // TODO: ":"?
1884
+ label: t("settings.clickingMode.label", { defaultValue: "Clicking mode:" }), // TODO: ":"?
1846
1885
  className: "tracky-mouse-clicking-mode",
1847
1886
  key: "clickingMode",
1848
1887
  type: "dropdown",
1849
1888
  options: [
1850
- { value: "dwell", label: t("Dwell to click"), description: t("Hold the cursor in place for a short time to click.") },
1851
- { 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." }) },
1852
1891
  // TODO: clarify that ooh works better than ah
1853
1892
  // "open wide" refers to height, but could be misinterpreted as opposite advice - a wide mouth shape when narrow works better
1854
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
1855
1894
  // Some people may understand "tall and narrow" better than "ooh rather than ah" and visa-versa
1856
- { 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.") },
1857
- { 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.") },
1858
- { 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.") },
1859
- { 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." }) },
1860
1899
  ],
1861
1900
  default: "dwell",
1862
- visible: () => isDesktopApp,
1863
- 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." }),
1864
1903
  },
1865
1904
  {
1866
1905
  // on Windows, currently, when buttons are swapped at the system level, it affects serenade-driver's click()
1867
1906
  // "swap" is purposefully generic language so we don't have to know what system-level setting is
1868
1907
  // (also this may be seen as a weirdly named/designed option for right-clicking with the dwell clicker)
1869
- label: t("Swap mouse buttons"),
1908
+ label: t("settings.swapMouseButtons.label", { defaultValue: "Swap mouse buttons" }),
1870
1909
  className: "tracky-mouse-swap-mouse-buttons",
1871
1910
  key: "swapMouseButtons",
1872
1911
  type: "checkbox",
1873
1912
  default: false,
1874
1913
  visible: () => isDesktopApp,
1875
- description: t(`Switches the left and right mouse buttons.
1914
+ description: t("settings.swapMouseButtons.description", {
1915
+ defaultValue: `Switches the left and right mouse buttons.
1876
1916
  Useful if your system's mouse buttons are swapped.
1877
- 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.` }),
1878
1918
  },
1879
1919
 
1880
1920
  // This setting could called "click stabilization", "drag delay", "delay before dragging", "click drag delay", "drag prevention", etc.
@@ -1883,33 +1923,37 @@ Could also be used to right click with the dwell clicker in a pinch.`),
1883
1923
  // at the end of the slider, although you shouldn't need to do that to effectively avoid dragging when trying to click,
1884
1924
  // and it might complicate the design of the slider labeling.
1885
1925
  {
1886
- label: t("Delay before dragging&nbsp;&nbsp;&nbsp;"), // TODO: avoid non-breaking space hack
1926
+ label: t("settings.delayBeforeDragging.label", { defaultValue: "Delay before dragging" }),
1887
1927
  className: "tracky-mouse-delay-before-dragging",
1888
1928
  key: "delayBeforeDragging",
1889
1929
  type: "slider",
1890
1930
  min: 0,
1891
1931
  max: 1000,
1892
1932
  labels: {
1893
- min: t("Easy to drag"),
1894
- 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" }),
1895
1935
  },
1896
1936
  default: 800,
1897
- visible: () => isDesktopApp,
1937
+ visible: () => isDesktopApp || clickingModeSupported,
1898
1938
  disabled: () => s.clickingMode === "off" || s.clickingMode === "dwell",
1899
- // description: t("Locks mouse movement during the start of a click to prevent accidental dragging."),
1900
- // description: t(`Prevents mouse movement for the specified time after a click starts.
1901
- // 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.`),
1902
- description: t(`Locks mouse movement for the given duration during the start of a click.
1903
- 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.` }),
1904
1948
  },
1905
1949
  ],
1906
1950
  },
1907
1951
  {
1908
1952
  type: "group",
1909
- label: t("Video"),
1953
+ label: t("settings.sections.video.label", { defaultValue: "Video" }),
1910
1954
  settings: [
1911
1955
  {
1912
- label: t("Camera source"),
1956
+ label: t("settings.cameraSource.label", { defaultValue: "Camera source" }),
1913
1957
  className: "tracky-mouse-camera-select",
1914
1958
  key: "cameraDeviceId",
1915
1959
  handleSettingChange: () => {
@@ -1917,15 +1961,15 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1917
1961
  },
1918
1962
  type: "dropdown",
1919
1963
  options: [
1920
- { value: "", label: t("Default") },
1964
+ { value: "", label: t("settings.cameraSource.defaultCamera", { defaultValue: "Default" }) },
1921
1965
  ],
1922
1966
  default: "",
1923
- // description: t("Select which camera to use for head tracking."),
1924
- 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." }),
1925
1969
  },
1926
1970
  // TODO: move this inline with the camera source dropdown?
1927
1971
  {
1928
- label: t("Open Camera Settings"),
1972
+ label: t("settings.openCameraSettings.label", { defaultValue: "Open Camera Settings" }),
1929
1973
  className: "tracky-mouse-open-camera-settings",
1930
1974
  key: "openCameraSettings",
1931
1975
  type: "button",
@@ -1935,45 +1979,45 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1935
1979
  try {
1936
1980
  knownCameras = JSON.parse(localStorage.getItem("tracky-mouse-known-cameras")) || {};
1937
1981
  } catch (error) {
1938
- 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);
1939
1983
  return;
1940
1984
  }
1941
1985
 
1942
1986
  const activeStream = cameraVideo.srcObject;
1943
1987
  const activeDeviceId = activeStream?.getVideoTracks()[0]?.getSettings()?.deviceId;
1944
- const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("Default");
1988
+ const selectedDeviceName = knownCameras[activeDeviceId]?.name || t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
1945
1989
 
1946
1990
  try {
1947
1991
  const result = await window.electronAPI.openCameraSettings(selectedDeviceName);
1948
1992
  if (result?.error) {
1949
- 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);
1950
1994
  }
1951
1995
  } catch (error) {
1952
- 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);
1953
1997
  }
1954
1998
  },
1955
- // description: t("Open your camera's system settings window to adjust properties like brightness and contrast."),
1956
- // description: t("Opens the system settings window for your camera to adjust properties like auto-focus and auto-exposure."),
1957
- 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." }),
1958
2002
  },
1959
2003
  // TODO: try moving this to the corner of the camera view, so it's clearer it applies only to the camera view
1960
2004
  {
1961
- label: t("Mirror"),
2005
+ label: t("settings.mirror.label", { defaultValue: "Mirror" }),
1962
2006
  className: "tracky-mouse-mirror",
1963
2007
  key: "mirror",
1964
2008
  type: "checkbox",
1965
2009
  default: true,
1966
- description: t("Mirrors the camera view horizontally."),
2010
+ description: t("settings.mirror.description", { defaultValue: "Mirrors the camera view horizontally." }),
1967
2011
  },
1968
2012
  ]
1969
2013
  },
1970
2014
  {
1971
2015
  type: "group",
1972
- label: t("General"),
2016
+ label: t("settings.sections.general.label", { defaultValue: "General" }),
1973
2017
  settings: [
1974
2018
  // opposite, "Start paused", might be clearer, especially if I add a "pause" button
1975
2019
  {
1976
- label: t("Start enabled"),
2020
+ label: t("settings.startEnabled.label", { defaultValue: "Start enabled" }),
1977
2021
  className: "tracky-mouse-start-enabled",
1978
2022
  key: "startEnabled",
1979
2023
  afterInitialLoad: () => { // TODO: does this hook make sense? right now it's the only usage. could this code not just be called later?
@@ -1981,10 +2025,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1981
2025
  },
1982
2026
  type: "checkbox",
1983
2027
  default: false,
1984
- description: t("If enabled, Tracky Mouse will start controlling the cursor as soon as it's launched."),
1985
- // description: t("Makes Tracky Mouse active when launched. Otherwise, you can start it manually when you're ready."),
1986
- // description: t("Makes Tracky Mouse active as soon as it's launched."),
1987
- // 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." }),
1988
2032
  },
1989
2033
  {
1990
2034
  // For "experimental" label:
@@ -1992,40 +2036,40 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
1992
2036
  // - I considered adding "⚠︎" but it feels a little too alarming
1993
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>)",
1994
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>)",
1995
- 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>)" }),
1996
2040
  className: "tracky-mouse-close-eyes-to-toggle",
1997
2041
  key: "closeEyesToToggle",
1998
2042
  type: "checkbox",
1999
2043
  default: false,
2000
- 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." }),
2001
2045
  },
2002
2046
  {
2003
- label: t("Run at login"),
2047
+ label: t("settings.runAtLogin.label", { defaultValue: "Run at login" }),
2004
2048
  className: "tracky-mouse-run-at-login",
2005
2049
  key: "runAtLogin",
2006
2050
  type: "checkbox",
2007
2051
  default: false,
2008
2052
  visible: () => isDesktopApp,
2009
- description: t("If enabled, Tracky Mouse will automatically start when you log into your computer."),
2010
- // 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." }),
2011
2055
  },
2012
2056
  {
2013
- label: t("Check for updates"),
2057
+ label: t("settings.checkForUpdates.label", { defaultValue: "Check for updates" }),
2014
2058
  className: "tracky-mouse-check-for-updates",
2015
2059
  key: "checkForUpdates",
2016
2060
  type: "checkbox",
2017
2061
  default: true,
2018
2062
  visible: () => isDesktopApp,
2019
- description: t("If enabled, Tracky Mouse will automatically check for updates when it starts."),
2020
- // description: t("Notifies you of new versions of Tracky Mouse."),
2021
- // 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." }),
2022
2066
  },
2023
2067
  {
2024
- label: t("Language"),
2068
+ label: t("settings.language.label", { defaultValue: "Language" }),
2025
2069
  className: "tracky-mouse-language",
2026
2070
  key: "language",
2027
2071
  type: "dropdown",
2028
- options: availableLanguages.map(lang => ({ value: lang, label: `${getLanguageEmoji(lang)} ${languageNames[lang]?.[1]?.[0] || lang} (${languageNames[lang]?.[0]?.[0] || "?"})` })),
2072
+ options: availableLanguages.map(lang => ({ value: lang, label: `${getLanguageFlagEmoji(lang)} ${languageNames[lang]?.[1]?.[0] || lang} (${languageNames[lang]?.[0]?.[0] || "?"})` })),
2029
2073
  default: locale,
2030
2074
  handleSettingChange: () => {
2031
2075
  // console.trace("handleSettingChange for language setting");
@@ -2039,8 +2083,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2039
2083
  }
2040
2084
  reinit();
2041
2085
  },
2042
- description: t("Select the language for the Tracky Mouse interface."),
2043
- // 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." }),
2044
2088
  },
2045
2089
  ],
2046
2090
  },
@@ -2162,7 +2206,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2162
2206
  </select>
2163
2207
  `;
2164
2208
  if (setting.options.some(option => option.description)) {
2165
- 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");
2166
2210
  }
2167
2211
  } else if (setting.type === "button") {
2168
2212
  rowEl.innerHTML = `
@@ -2476,6 +2520,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2476
2520
  func();
2477
2521
  }
2478
2522
 
2523
+ // Unstable hook
2524
+ handleSettingsUpdate?.(settings);
2479
2525
  }
2480
2526
  const formatVersion = 1;
2481
2527
  const formatName = "tracky-mouse-settings";
@@ -2504,17 +2550,43 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2504
2550
  console.error(e);
2505
2551
  }
2506
2552
  }
2553
+ // Unstable hook
2554
+ handleSettingsUpdate?.(options);
2507
2555
  };
2508
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;
2509
2568
  if (window.electronAPI) {
2510
- deserializeSettings(await window.electronAPI.getOptions(), initialLoad);
2569
+ stored = await window.electronAPI.getOptions();
2511
2570
  } else {
2512
2571
  try {
2513
2572
  if (localStorage.getItem("tracky-mouse-settings")) {
2514
- deserializeSettings(JSON.parse(localStorage.getItem("tracky-mouse-settings")), initialLoad);
2573
+ stored = JSON.parse(localStorage.getItem("tracky-mouse-settings"));
2515
2574
  }
2516
2575
  } catch (e) {
2517
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());
2518
2590
  }
2519
2591
  }
2520
2592
  };
@@ -2565,14 +2637,14 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2565
2637
 
2566
2638
  const defaultOption = document.createElement("option");
2567
2639
  defaultOption.value = "";
2568
- defaultOption.text = t("Default");
2640
+ defaultOption.text = t("settings.cameraSource.defaultCamera", { defaultValue: "Default" });
2569
2641
  cameraSelect.appendChild(defaultOption);
2570
2642
 
2571
2643
  let matchingDeviceId = "";
2572
2644
  for (const device of videoDevices) {
2573
2645
  const option = document.createElement('option');
2574
2646
  option.value = device.deviceId;
2575
- 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);
2576
2648
  cameraSelect.appendChild(option);
2577
2649
  if (device.deviceId === s.cameraDeviceId) {
2578
2650
  matchingDeviceId = device.deviceId;
@@ -2590,7 +2662,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2590
2662
  const option = document.createElement("option");
2591
2663
  option.value = s.cameraDeviceId;
2592
2664
  const knownInfo = knownCameras[s.cameraDeviceId];
2593
- 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" });
2594
2666
  cameraSelect.appendChild(option);
2595
2667
  cameraSelect.value = s.cameraDeviceId;
2596
2668
  } else {
@@ -2652,6 +2724,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2652
2724
  updateStartStopButton();
2653
2725
  };
2654
2726
 
2727
+ let showedCameraError = false;
2655
2728
  useCameraButton.onclick = TrackyMouse.useCamera = async (optionsOrEvent = {}) => {
2656
2729
  // Phases:
2657
2730
  // 1. "tryPreferredCamera"
@@ -2776,7 +2849,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2776
2849
  }
2777
2850
  if (error.name == "NotFoundError" || error.name == "DevicesNotFoundError") {
2778
2851
  // required track is missing
2779
- 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." });
2780
2853
  } else if (error.name == "NotReadableError" || error.name == "TrackStartError") {
2781
2854
  // webcam is already in use
2782
2855
  // or: OBS Virtual Camera is present but OBS is not running with Virtual Camera started
@@ -2784,7 +2857,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2784
2857
  // (listing devices and showing only the OBS Virtual Camera would also be a good clue in itself;
2785
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
2786
2859
  // or "1 camera source detected" preceding it)
2787
- 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." });
2788
2861
  } else if (error.name === "AbortError") {
2789
2862
  // webcam is likely already in use
2790
2863
  // I observed AbortError in Firefox 132.0.2 but I don't know it's used exclusively for this case.
@@ -2792,7 +2865,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2792
2865
  // Like, it might have to do with permissions being denied outside of a user gesture (distinct from the user denying the permission)
2793
2866
  // I really hope that isn't the problem.
2794
2867
  // errorMessage.textContent = "Webcam may already be in use. Please make sure you have no other programs using the camera.";
2795
- 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." });
2796
2869
  // A more honest/helpful message might be:
2797
2870
  // errorMessage.textContent = "Please try again and then make sure no other programs are using the camera and try again again.";
2798
2871
  // errorMessage.textContent = "Please try again before/after making sure no other programs are using the camera.";
@@ -2810,25 +2883,32 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
2810
2883
  // errorMessage.textContent = "The previously selected camera is not available. Please mess around with Video > Camera source.";
2811
2884
  // errorMessage.textContent = "The previously selected camera is not available. Try changing Video > Camera source.";
2812
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.";
2813
- 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." });
2814
2887
  // It's awkward but that's my best attempt at conveying how you may need to proceed
2815
2888
  // without complicated description of how/why the dropdown might be populated with
2816
2889
  // fake information until a camera stream is successfully opened.
2817
2890
  } else {
2818
- 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." });
2819
2892
  }
2820
2893
  } else if (error.name == "NotAllowedError" || error.name == "PermissionDeniedError") {
2821
2894
  // permission denied in browser
2822
- 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." });
2823
2896
  } else if (error.name == "TypeError") {
2824
2897
  // empty constraints object
2825
- 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})`;
2826
2899
  } else {
2827
2900
  // other errors
2828
- 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})`;
2829
2902
  }
2830
- errorMessage.textContent = `${t("⚠️ ")}${errorMessage.textContent}`;
2903
+ errorMessage.textContent = `${t("common.warningIcon", { defaultValue: "⚠️" })} ${errorMessage.textContent}`;
2831
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;
2832
2912
  });
2833
2913
  };
2834
2914
  useDemoFootageButton.onclick = TrackyMouse.useDemoFootage = () => {
@@ -3535,13 +3615,10 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3535
3615
  }
3536
3616
  }
3537
3617
 
3538
- // TODO: implement these clicking modes for the web library version
3539
- // and unhide the "Clicking mode" setting in the UI
3540
- // https://github.com/1j01/tracky-mouse/issues/72
3541
3618
  const buttonNames = ["left", "middle", "right"];
3542
3619
  for (let buttonIndex = 0; buttonIndex < 3; buttonIndex++) {
3543
3620
  if ((clickButton === buttonIndex) !== buttonStates[buttonNames[buttonIndex]]) {
3544
- window.electronAPI?.setMouseButtonState(buttonIndex, clickButton === buttonIndex);
3621
+ setMouseButtonState(buttonIndex, clickButton === buttonIndex);
3545
3622
  buttonStates[buttonNames[buttonIndex]] = clickButton === buttonIndex;
3546
3623
  if ((clickButton === buttonIndex)) {
3547
3624
  lastMouseDownTime = performance.now();
@@ -3562,13 +3639,11 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3562
3639
  pointTracker.update(imageData);
3563
3640
  }
3564
3641
 
3565
- if (window.electronAPI) {
3566
- window.electronAPI.updateInputFeedback({
3567
- headNotFound: !face && !facemeshPrediction,
3568
- blinkInfo,
3569
- mouthInfo,
3570
- });
3571
- }
3642
+ updateInputFeedback?.({
3643
+ headNotFound: !face && !facemeshPrediction,
3644
+ blinkInfo,
3645
+ mouthInfo,
3646
+ });
3572
3647
 
3573
3648
  if (facemeshPrediction) {
3574
3649
  ctx.fillStyle = "red";
@@ -3613,17 +3688,20 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3613
3688
  const textYStart = -10;
3614
3689
 
3615
3690
 
3616
- const pitchText = t("Pitch: ") + `${(headTilt.pitch * 180 / Math.PI).toFixed(1)}°`;
3617
- const yawText = t("Yaw: ") + `${(headTilt.yaw * 180 / Math.PI).toFixed(1)}°`;
3618
- const rollText = t("Roll: ") + `${(headTilt.roll * 180 / Math.PI).toFixed(1)}°`;
3619
-
3620
- const boxWidth = Math.max(
3621
- ctx.measureText(pitchText).width,
3622
- ctx.measureText(yawText).width,
3623
- ctx.measureText(rollText).width
3624
- );
3625
- const boxHeight = textLineHeight * 3;
3626
- 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;
3627
3705
 
3628
3706
  // Calculate screen coordinates for the text box
3629
3707
  let screenX = s.mirror ? canvas.width - cx : cx;
@@ -3634,7 +3712,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3634
3712
  let textScreenY = screenY + textYStart;
3635
3713
 
3636
3714
  // Clamp to canvas bounds
3637
- textScreenX = Math.max(padding, Math.min(canvas.width - boxWidth - padding, textScreenX));
3715
+ textScreenX = Math.max(boxPadding, Math.min(canvas.width - boxWidth - boxPadding, textScreenX));
3638
3716
  textScreenY = Math.max(textLineHeight, Math.min(canvas.height - boxHeight + textLineHeight, textScreenY));
3639
3717
 
3640
3718
  ctx.save();
@@ -3648,12 +3726,18 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3648
3726
  const dx = textScreenX - screenNoseX;
3649
3727
  const dy = textScreenY - screenNoseY;
3650
3728
 
3651
- ctx.strokeText(pitchText, dx, dy);
3652
- ctx.fillText(pitchText, dx, dy);
3653
- ctx.strokeText(yawText, dx, dy + textLineHeight);
3654
- ctx.fillText(yawText, dx, dy + textLineHeight);
3655
- ctx.strokeText(rollText, dx, dy + textLineHeight * 2);
3656
- 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
+ }
3657
3741
 
3658
3742
  ctx.restore();
3659
3743
 
@@ -3986,8 +4070,8 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
3986
4070
  pointerEl.style.display = "none";
3987
4071
  } else {
3988
4072
  pointerEl.style.display = "";
3989
- pointerEl.style.left = `${mouseX}px`;
3990
- pointerEl.style.top = `${mouseY}px`;
4073
+ pointerEl.style.left = `${Math.floor(mouseX)}px`;
4074
+ pointerEl.style.top = `${Math.floor(mouseY)}px`;
3991
4075
  }
3992
4076
  if (TrackyMouse.onPointerMove) {
3993
4077
  TrackyMouse.onPointerMove(mouseX, mouseY);
@@ -4003,9 +4087,9 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4003
4087
  ctx.lineWidth = 3;
4004
4088
  ctx.font = "20px sans-serif";
4005
4089
  ctx.beginPath();
4006
- const text3 = t("Face convergence score: ") + ((useFacemesh && facemeshPrediction) ? t("N/A") : faceConvergence.toFixed(4));
4007
- const text1 = t("Face tracking score: ") + ((useFacemesh && facemeshPrediction) ? facemeshPrediction.faceInViewConfidence : faceScore).toFixed(4);
4008
- 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)}`;
4009
4093
  ctx.strokeText(text1, 50, 50);
4010
4094
  ctx.fillText(text1, 50, 50);
4011
4095
  ctx.strokeText(text2, 50, 70);
@@ -4041,14 +4125,26 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4041
4125
  TrackyMouse.useDemoFootage();
4042
4126
  } else if (window.electronAPI) {
4043
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
+ });
4044
4140
  }
4045
4141
 
4046
4142
  const updateStartStopButton = () => {
4047
4143
  if (paused) {
4048
- startStopButton.textContent = t("Start");
4144
+ startStopButton.textContent = t("ui.startStopButton.start", { defaultValue: "Start" });
4049
4145
  startStopButton.setAttribute("aria-pressed", "false");
4050
4146
  } else {
4051
- startStopButton.textContent = t("Stop");
4147
+ startStopButton.textContent = t("ui.startStopButton.stop", { defaultValue: "Stop" });
4052
4148
  startStopButton.setAttribute("aria-pressed", "true");
4053
4149
  }
4054
4150
  };
@@ -4058,9 +4154,7 @@ You may want to turn this off if you're drawing on a canvas, or increase it if y
4058
4154
  pointerEl.style.display = "none";
4059
4155
  }
4060
4156
  updateStartStopButton();
4061
- if (window.electronAPI) {
4062
- window.electronAPI.notifyToggleState(!paused);
4063
- }
4157
+ notifyToggleState?.(!paused);
4064
4158
  };
4065
4159
  const handleShortcut = (shortcutType) => {
4066
4160
  if (shortcutType === "toggle-tracking") {
@@ -4202,15 +4296,17 @@ TrackyMouse.init = function (div, opts = {}) {
4202
4296
  TrackyMouse.initScreenOverlay = () => {
4203
4297
 
4204
4298
  const template = `
4299
+ <div class="tracky-mouse-hide-near-cursor">
4205
4300
  <div class="tracky-mouse-absolute-center">
4206
4301
  <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-manual-takeback-indicator">
4207
- <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">
4208
4303
  </div>
4209
4304
  <div class="tracky-mouse-screen-overlay-status-indicator tracky-mouse-head-not-found-indicator">
4210
- <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">
4211
4306
  </div>
4212
4307
  </div>
4213
4308
  <div id="tracky-mouse-screen-overlay-message"></div>
4309
+ </div>
4214
4310
  `;
4215
4311
  const fragment = document.createRange().createContextualFragment(template);
4216
4312
  document.body.appendChild(fragment);
@@ -4218,8 +4314,11 @@ TrackyMouse.initScreenOverlay = () => {
4218
4314
  const message = document.getElementById("tracky-mouse-screen-overlay-message");
4219
4315
  message.dir = "auto";
4220
4316
 
4317
+ const hideNearCursorEls = document.querySelectorAll(".tracky-mouse-hide-near-cursor");
4318
+
4221
4319
  const inputFeedbackCanvas = document.createElement("canvas");
4222
- inputFeedbackCanvas.style.position = "absolute";
4320
+ inputFeedbackCanvas.style.position = "fixed";
4321
+ inputFeedbackCanvas.style.zIndex = "899990"; // just below .tracky-mouse-pointer
4223
4322
  inputFeedbackCanvas.style.top = "0";
4224
4323
  inputFeedbackCanvas.style.left = "0";
4225
4324
  inputFeedbackCanvas.style.pointerEvents = "none";
@@ -4255,10 +4354,11 @@ TrackyMouse.initScreenOverlay = () => {
4255
4354
  // inputFeedbackCanvas.style.transform = `translate(${x - inputFeedbackCanvas.width / 2}px, ${y - inputFeedbackCanvas.height / 2}px)`;
4256
4355
  // inputFeedbackCanvas.style.transform = `translate(${x}px, ${y}px)`;
4257
4356
  inputFeedbackCanvas.style.transform = `translate(${Math.min(x, window.innerWidth - inputFeedbackCanvas.width)}px, ${Math.min(y, window.innerHeight - inputFeedbackCanvas.height)}px)`;
4357
+
4258
4358
  }
4259
4359
 
4260
4360
  function update(data) {
4261
- const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset } = data;
4361
+ const { messageText, isEnabled, isManualTakeback, inputFeedback, bottomOffset, systemMousePosition } = data;
4262
4362
 
4263
4363
  message.style.bottom = `${bottomOffset}px`;
4264
4364
 
@@ -4267,21 +4367,31 @@ TrackyMouse.initScreenOverlay = () => {
4267
4367
  // - bad lighting conditions
4268
4368
  // see: https://github.com/1j01/tracky-mouse/issues/26
4269
4369
 
4270
- document.body.classList.toggle("tracky-mouse-manual-takeback", isManualTakeback);
4271
- 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);
4272
4372
 
4273
4373
  message.innerText = messageText;
4274
4374
 
4275
4375
  if (!isEnabled && !isManualTakeback) {
4276
4376
  // Fade out the message after a little while so it doesn't get in the way.
4277
4377
  // TODO: make sure animation isn't interrupted by inputFeedback updates.
4278
- 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";
4279
4379
  } else {
4280
4380
  message.style.animation = "";
4281
4381
  message.style.opacity = "1";
4282
4382
  }
4283
4383
 
4284
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
+ }
4285
4395
  }
4286
4396
 
4287
4397
  return {