@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.3

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 (373) hide show
  1. package/AGENTS.md +556 -0
  2. package/INSTALLATION.md +310 -0
  3. package/LICENSE +21 -0
  4. package/README.md +195 -2
  5. package/client.js +3 -0
  6. package/components.json +20 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/README.en.md +196 -0
  9. package/docs/screenshots/en/overview.png +0 -0
  10. package/docs/screenshots/en/question.png +0 -0
  11. package/docs/screenshots/zh/overview.png +0 -0
  12. package/docs/screenshots/zh/question.png +0 -0
  13. package/icon.svg +24 -0
  14. package/index.js +2 -0
  15. package/lib/account.js +1 -0
  16. package/lib/app-mapping.js +1 -0
  17. package/lib/i18n.js +1 -0
  18. package/lib/media-enrich.js +1 -0
  19. package/lib/notifications.js +1 -0
  20. package/lib/performance.js +1 -0
  21. package/lib/platform/index.js +1 -0
  22. package/lib/probe-adapter.js +1 -0
  23. package/lib/probe-daemon.js +1 -0
  24. package/lib/probe.js +1 -0
  25. package/lib/qr.js +1 -0
  26. package/lib/runtime.js +1 -0
  27. package/lib/service.js +1 -0
  28. package/lib/sessions.js +1 -0
  29. package/lib/settings.js +1 -0
  30. package/lib/types.js +1 -0
  31. package/lib/util.js +1 -0
  32. package/lib/weather.js +1 -0
  33. package/lib/ws.js +1 -0
  34. package/locale/en.json +6 -0
  35. package/locale/zh.json +6 -0
  36. package/package.json +118 -4
  37. package/scripts/browser-shot.mjs +217 -0
  38. package/scripts/build-dsh.mjs +228 -0
  39. package/scripts/build-rust.mjs +319 -0
  40. package/scripts/build-web.mjs +17 -0
  41. package/scripts/check-npm-package.mjs +322 -0
  42. package/scripts/mock-preview.py +574 -0
  43. package/scripts/screenshot-compose.py +89 -0
  44. package/scripts/screenshot-device.sh +75 -0
  45. package/src/dsh/client/api.ts +68 -0
  46. package/src/dsh/client/i18n.ts +573 -0
  47. package/src/dsh/client/icons.ts +56 -0
  48. package/src/dsh/client/index.ts +95 -0
  49. package/src/dsh/client/logic.ts +310 -0
  50. package/src/dsh/client/panel.ts +483 -0
  51. package/src/dsh/client/sections/apps.ts +40 -0
  52. package/src/dsh/client/sections/balance.ts +38 -0
  53. package/src/dsh/client/sections/connection.ts +97 -0
  54. package/src/dsh/client/sections/notifications.ts +43 -0
  55. package/src/dsh/client/sections/pomodoro.ts +184 -0
  56. package/src/dsh/client/sections/preferences.ts +138 -0
  57. package/src/dsh/client/sections/system.ts +120 -0
  58. package/src/dsh/client/sections/weather.ts +276 -0
  59. package/src/dsh/client/styles.ts +1119 -0
  60. package/src/dsh/client/types.ts +366 -0
  61. package/src/dsh/client/ui.ts +142 -0
  62. package/src/dsh/index.ts +108 -0
  63. package/src/dsh/lib/account.ts +520 -0
  64. package/src/dsh/lib/app-mapping.ts +366 -0
  65. package/src/dsh/lib/i18n.ts +82 -0
  66. package/src/dsh/lib/media-enrich.ts +202 -0
  67. package/src/dsh/lib/notifications.ts +322 -0
  68. package/src/dsh/lib/performance.ts +279 -0
  69. package/src/dsh/lib/platform/index.ts +43 -0
  70. package/src/dsh/lib/probe-adapter.ts +275 -0
  71. package/src/dsh/lib/probe-daemon.ts +359 -0
  72. package/src/dsh/lib/probe.ts +395 -0
  73. package/src/dsh/lib/qr.ts +42 -0
  74. package/src/dsh/lib/runtime.ts +126 -0
  75. package/src/dsh/lib/service.ts +1645 -0
  76. package/src/dsh/lib/sessions.ts +907 -0
  77. package/src/dsh/lib/settings.ts +468 -0
  78. package/src/dsh/lib/types.ts +231 -0
  79. package/src/dsh/lib/util.ts +115 -0
  80. package/src/dsh/lib/weather.ts +532 -0
  81. package/src/dsh/lib/ws.ts +328 -0
  82. package/src/dsh/types/qrcode-generator.d.ts +16 -0
  83. package/src/rust/Cargo.lock +1431 -0
  84. package/src/rust/Cargo.toml +68 -0
  85. package/src/rust/src/cli.rs +233 -0
  86. package/src/rust/src/daemon.rs +140 -0
  87. package/src/rust/src/location/linux.rs +118 -0
  88. package/src/rust/src/location/macos.rs +26 -0
  89. package/src/rust/src/location/mod.rs +170 -0
  90. package/src/rust/src/location/windows.rs +205 -0
  91. package/src/rust/src/main.rs +42 -0
  92. package/src/rust/src/media/catalog.rs +454 -0
  93. package/src/rust/src/media/linux.rs +350 -0
  94. package/src/rust/src/media/macos/accessibility.rs +59 -0
  95. package/src/rust/src/media/macos.rs +366 -0
  96. package/src/rust/src/media/mod.rs +298 -0
  97. package/src/rust/src/media/vendor/apple_music.rs +90 -0
  98. package/src/rust/src/media/vendor/common.rs +186 -0
  99. package/src/rust/src/media/vendor/kugou.rs +52 -0
  100. package/src/rust/src/media/vendor/kuwo.rs +49 -0
  101. package/src/rust/src/media/vendor/mod.rs +75 -0
  102. package/src/rust/src/media/vendor/netease.rs +1715 -0
  103. package/src/rust/src/media/vendor/qishui.rs +36 -0
  104. package/src/rust/src/media/vendor/qqmusic.rs +33 -0
  105. package/src/rust/src/media/vendor/spotify.rs +46 -0
  106. package/src/rust/src/media/vendor/wesing.rs +39 -0
  107. package/src/rust/src/media/vendor/youtube_music.rs +49 -0
  108. package/src/rust/src/media/windows/accessibility.rs +85 -0
  109. package/src/rust/src/media/windows/audio.rs +116 -0
  110. package/src/rust/src/media/windows/private.rs +291 -0
  111. package/src/rust/src/media/windows.rs +775 -0
  112. package/src/rust/src/notifications/linux.rs +276 -0
  113. package/src/rust/src/notifications/macos.rs +382 -0
  114. package/src/rust/src/notifications/mod.rs +244 -0
  115. package/src/rust/src/notifications/windows.rs +565 -0
  116. package/src/rust/src/perf/common.rs +109 -0
  117. package/src/rust/src/perf/linux.rs +282 -0
  118. package/src/rust/src/perf/macos.rs +28 -0
  119. package/src/rust/src/perf/mod.rs +157 -0
  120. package/src/rust/src/perf/windows.rs +981 -0
  121. package/src/rust/src/util.rs +360 -0
  122. package/src/rust/src/volume/linux.rs +195 -0
  123. package/src/rust/src/volume/macos.rs +54 -0
  124. package/src/rust/src/volume/mod.rs +116 -0
  125. package/src/rust/src/volume/windows.rs +75 -0
  126. package/src/rust/tests/cli.rs +79 -0
  127. package/src/rust/tests/daemon.rs +107 -0
  128. package/src/svelte/App.svelte +385 -0
  129. package/src/svelte/index.html +18 -0
  130. package/src/svelte/lib/billing.svelte.ts +228 -0
  131. package/src/svelte/lib/components/BalanceCard.svelte +173 -0
  132. package/src/svelte/lib/components/ClockCard.svelte +183 -0
  133. package/src/svelte/lib/components/CountdownCard.svelte +261 -0
  134. package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
  135. package/src/svelte/lib/components/EmptyState.svelte +34 -0
  136. package/src/svelte/lib/components/GpuCard.svelte +245 -0
  137. package/src/svelte/lib/components/HomePage.svelte +34 -0
  138. package/src/svelte/lib/components/MusicControls.svelte +256 -0
  139. package/src/svelte/lib/components/MusicCover.svelte +88 -0
  140. package/src/svelte/lib/components/MusicPage.svelte +116 -0
  141. package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
  142. package/src/svelte/lib/components/Panel.svelte +22 -0
  143. package/src/svelte/lib/components/PerformancePage.svelte +55 -0
  144. package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
  145. package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
  146. package/src/svelte/lib/components/RadialGauge.svelte +99 -0
  147. package/src/svelte/lib/components/Rail.svelte +217 -0
  148. package/src/svelte/lib/components/SectionHeader.svelte +40 -0
  149. package/src/svelte/lib/components/SeekBar.svelte +421 -0
  150. package/src/svelte/lib/components/SessionList.svelte +364 -0
  151. package/src/svelte/lib/components/SessionStats.svelte +98 -0
  152. package/src/svelte/lib/components/SessionsPage.svelte +61 -0
  153. package/src/svelte/lib/components/SpendCard.svelte +105 -0
  154. package/src/svelte/lib/components/ThermalCard.svelte +249 -0
  155. package/src/svelte/lib/components/Toast.svelte +32 -0
  156. package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
  157. package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
  158. package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
  159. package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
  160. package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
  161. package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
  162. package/src/svelte/lib/components/ui/alert/index.ts +17 -0
  163. package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
  164. package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
  165. package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
  166. package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
  167. package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
  168. package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
  169. package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
  170. package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
  171. package/src/svelte/lib/components/ui/badge/index.ts +2 -0
  172. package/src/svelte/lib/components/ui/button/button.svelte +82 -0
  173. package/src/svelte/lib/components/ui/button/index.ts +17 -0
  174. package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
  175. package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
  176. package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
  177. package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
  178. package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
  179. package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
  180. package/src/svelte/lib/components/ui/card/card.svelte +22 -0
  181. package/src/svelte/lib/components/ui/card/index.ts +25 -0
  182. package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
  183. package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
  184. package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
  185. package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
  186. package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
  187. package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
  188. package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  189. package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
  190. package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
  191. package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
  192. package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
  193. package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
  194. package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
  195. package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
  196. package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
  197. package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
  198. package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
  199. package/src/svelte/lib/components/ui/empty/index.ts +22 -0
  200. package/src/svelte/lib/components/ui/input/index.ts +7 -0
  201. package/src/svelte/lib/components/ui/input/input.svelte +48 -0
  202. package/src/svelte/lib/components/ui/progress/index.ts +7 -0
  203. package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
  204. package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
  205. package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
  206. package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
  207. package/src/svelte/lib/components/ui/separator/index.ts +7 -0
  208. package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
  209. package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
  210. package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
  211. package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
  212. package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
  213. package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  214. package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
  215. package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  216. package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  217. package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
  218. package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  219. package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
  220. package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
  221. package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
  222. package/src/svelte/lib/components/ui/slider/index.ts +7 -0
  223. package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
  224. package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
  225. package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
  226. package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  227. package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  228. package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  229. package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
  230. package/src/svelte/lib/countdown.svelte.ts +252 -0
  231. package/src/svelte/lib/format.ts +199 -0
  232. package/src/svelte/lib/i18n.svelte.ts +583 -0
  233. package/src/svelte/lib/media.ts +32 -0
  234. package/src/svelte/lib/pages.ts +18 -0
  235. package/src/svelte/lib/pomodoro.svelte.ts +223 -0
  236. package/src/svelte/lib/session-tree.ts +80 -0
  237. package/src/svelte/lib/state.svelte.ts +359 -0
  238. package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
  239. package/src/svelte/lib/types.ts +306 -0
  240. package/src/svelte/lib/utils.ts +8 -0
  241. package/src/svelte/lib/weather.ts +84 -0
  242. package/src/svelte/lib/ws.ts +104 -0
  243. package/src/svelte/main.ts +13 -0
  244. package/src/svelte/styles/global.css +315 -0
  245. package/src/svelte/vite-env.d.ts +12 -0
  246. package/static/fonts/LICENSE.txt +287 -0
  247. package/static/icon.svg +24 -0
  248. package/static/manifest.webmanifest +18 -0
  249. package/svelte.config.js +5 -0
  250. package/tsconfig.dsh-client.json +18 -0
  251. package/tsconfig.dsh.json +21 -0
  252. package/tsconfig.json +20 -0
  253. package/vite.config.js +31 -0
  254. package/web/assets/index-DLdSfnJ3.css +1 -0
  255. package/web/assets/index-YNE8X0Uy.js +6 -0
  256. package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  257. package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  258. package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  259. package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  260. package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  261. package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  262. package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  263. package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  264. package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  265. package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  266. package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  267. package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  268. package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  269. package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
  270. package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
  271. package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
  272. package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
  273. package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
  274. package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
  275. package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
  276. package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
  277. package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
  278. package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
  279. package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
  280. package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
  281. package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
  282. package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
  283. package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
  284. package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
  285. package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
  286. package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
  287. package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
  288. package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
  289. package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
  290. package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
  291. package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
  292. package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
  293. package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
  294. package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
  295. package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
  296. package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
  297. package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
  298. package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
  299. package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
  300. package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
  301. package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
  302. package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
  303. package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
  304. package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
  305. package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
  306. package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
  307. package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
  308. package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
  309. package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
  310. package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
  311. package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
  312. package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
  313. package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
  314. package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
  315. package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
  316. package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
  317. package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
  318. package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
  319. package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
  320. package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
  321. package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
  322. package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
  323. package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
  324. package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
  325. package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
  326. package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
  327. package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
  328. package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
  329. package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
  330. package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
  331. package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
  332. package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
  333. package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
  334. package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
  335. package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
  336. package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
  337. package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
  338. package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
  339. package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
  340. package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
  341. package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
  342. package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
  343. package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
  344. package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
  345. package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
  346. package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
  347. package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
  348. package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
  349. package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
  350. package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
  351. package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
  352. package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
  353. package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
  354. package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
  355. package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
  356. package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
  357. package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
  358. package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
  359. package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
  360. package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
  361. package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
  362. package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
  363. package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
  364. package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
  365. package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
  366. package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
  367. package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
  368. package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
  369. package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
  370. package/web/fonts/LICENSE.txt +287 -0
  371. package/web/icon.svg +24 -0
  372. package/web/index.html +19 -0
  373. package/web/manifest.webmanifest +18 -0
@@ -0,0 +1,468 @@
1
+ /** Persistent plugin settings for the remote console. */
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import type { JsonRecord } from './types.js';
8
+ import { isRecord } from './types.js';
9
+
10
+ const SETTINGS_VERSION = 1;
11
+ const PAGE_KEYS = ['home', 'sessions', 'music', 'performance'] as const;
12
+
13
+ export type PageKey = (typeof PAGE_KEYS)[number];
14
+ export type ThemeSetting = 'dsh' | 'light' | 'dark';
15
+ export type DshTheme = 'light' | 'dark';
16
+ export type TemperatureUnit = 'celsius' | 'fahrenheit';
17
+ export type ForecastMode = 'hourly' | 'daily';
18
+ export type SpendWindow = 'calendar' | 'rolling';
19
+ export type NavPosition = 'left' | 'right' | 'top' | 'bottom';
20
+ /** Plugin language preference: follow DSH, or force one supported language. */
21
+ export type LanguageSetting = 'dsh' | 'zh' | 'en';
22
+ /** Languages the plugin ships UI translations for. */
23
+ export const SUPPORTED_LOCALES = ['zh', 'en'] as const;
24
+ export type PluginLocale = (typeof SUPPORTED_LOCALES)[number];
25
+ /** DSH locale ids are BCP 47-style tags; anything else is ignored. */
26
+ const LOCALE_ID_PATTERN = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
27
+
28
+ /**
29
+ * Resolve the console language.
30
+ *
31
+ * An explicit plugin choice wins. Following DSH requires an exact match against
32
+ * the languages this plugin actually translates; a DSH locale such as `ja` or a
33
+ * regional tag without its own bundle falls back to English rather than
34
+ * borrowing an unrelated dictionary.
35
+ */
36
+ export function resolveLocale(language: LanguageSetting, dshLocale: string): PluginLocale {
37
+ if (language === 'zh' || language === 'en') return language;
38
+ const requested = String(dshLocale || '').trim().toLowerCase();
39
+ const exact = SUPPORTED_LOCALES.find((id) => id === requested);
40
+ return exact ?? 'en';
41
+ }
42
+
43
+ /** Normalize a DSH locale id pushed by the browser panel. */
44
+ export function normalizeDshLocale(value: unknown): string {
45
+ if (typeof value !== 'string') return '';
46
+ const trimmed = value.trim();
47
+ if (trimmed.length > 35 || !LOCALE_ID_PATTERN.test(trimmed)) return '';
48
+ return trimmed.toLowerCase();
49
+ }
50
+
51
+ export interface WeatherSettings {
52
+ mode: 'auto' | 'manual';
53
+ name: string;
54
+ latitude: number | null;
55
+ longitude: number | null;
56
+ forecastMode: ForecastMode;
57
+ }
58
+
59
+ export interface BalanceSettings {
60
+ spendWindow: SpendWindow;
61
+ lowThreshold: number | null;
62
+ /** Show the peak/valley tariff icon on the console balance card. */
63
+ showPeakValley: boolean;
64
+ }
65
+
66
+ export interface NotificationSettings {
67
+ enabledApps: string[];
68
+ limit: number;
69
+ /** Push complete pending questions so the phone can answer them. */
70
+ pushQuestions: boolean;
71
+ }
72
+
73
+ export interface TlsSettings {
74
+ enabled: boolean;
75
+ certPath: string;
76
+ keyPath: string;
77
+ }
78
+
79
+ /** Browser-side tomato timer configuration; only the console runs the timer. */
80
+ export interface PomodoroSettings {
81
+ enabled: boolean;
82
+ /** Focus length in minutes; two decimal places are allowed. */
83
+ focusMinutes: number;
84
+ /** Break length in minutes; two decimal places are allowed. */
85
+ breakMinutes: number;
86
+ soundPath: string;
87
+ /** Keep ringing until the console user explicitly dismisses the alarm. */
88
+ loopAlarm: boolean;
89
+ }
90
+
91
+ /** Browser-side countdown configuration; the console runs the timer itself. */
92
+ export interface CountdownSettings {
93
+ enabled: boolean;
94
+ /** Quick-select durations in seconds; one to {@link COUNTDOWN_PRESET_LIMIT} entries. */
95
+ presets: number[];
96
+ }
97
+
98
+ /** Upper bound for user-configured countdown presets. */
99
+ export const COUNTDOWN_PRESET_LIMIT = 5;
100
+ /** Smallest and largest accepted countdown duration, in seconds. */
101
+ export const COUNTDOWN_MIN_SECONDS = 1;
102
+ export const COUNTDOWN_MAX_SECONDS = 24 * 60 * 60;
103
+ /** Smallest and largest accepted pomodoro duration, in minutes. */
104
+ export const POMODORO_MIN_MINUTES = 0.1;
105
+ export const POMODORO_MAX_FOCUS_MINUTES = 180;
106
+ export const POMODORO_MAX_BREAK_MINUTES = 120;
107
+ /** Presets a fresh profile starts with; the user may keep one to five. */
108
+ export const DEFAULT_COUNTDOWN_PRESETS: readonly number[] = [60, 300, 600];
109
+
110
+ export interface RemoteControlSettings {
111
+ version: number;
112
+ token: string;
113
+ tokenRotatedAt: number;
114
+ tokenEnabled: boolean;
115
+ allowedInterfaces: string[] | null;
116
+ theme: ThemeSetting;
117
+ dshTheme: DshTheme;
118
+ /** Plugin UI language: follow DSH or force a shipped language. */
119
+ language: LanguageSetting;
120
+ /** Active DSH locale pushed by the sidebar; empty until the first sync. */
121
+ dshLocale: string;
122
+ temperatureUnit: TemperatureUnit;
123
+ weather: WeatherSettings;
124
+ balance: BalanceSettings;
125
+ notifications: NotificationSettings;
126
+ tls: TlsSettings;
127
+ navPosition: NavPosition;
128
+ pomodoro: PomodoroSettings;
129
+ countdown: CountdownSettings;
130
+ pageOrder: PageKey[];
131
+ pageEnabled: Record<PageKey, boolean>;
132
+ updatedAt: number;
133
+ }
134
+
135
+ export interface PublicSettings {
136
+ tokenEnabled: boolean;
137
+ tokenRotatedAt: number;
138
+ allowedInterfaces: string[] | null;
139
+ theme: ThemeSetting;
140
+ effectiveTheme: DshTheme;
141
+ language: LanguageSetting;
142
+ /** Resolved console language for the phone page. */
143
+ locale: PluginLocale;
144
+ temperatureUnit: TemperatureUnit;
145
+ weather: WeatherSettings;
146
+ balance: BalanceSettings;
147
+ notifications: NotificationSettings;
148
+ tls: TlsSettings;
149
+ navPosition: NavPosition;
150
+ pomodoro: PomodoroSettings;
151
+ countdown: CountdownSettings;
152
+ pageOrder: PageKey[];
153
+ pageEnabled: Record<PageKey, boolean>;
154
+ updatedAt: number;
155
+ }
156
+
157
+ export interface SettingsStore {
158
+ readonly dir: string;
159
+ readonly file: string;
160
+ get(): RemoteControlSettings;
161
+ public(): PublicSettings;
162
+ update(patch: unknown): RemoteControlSettings;
163
+ save(next: unknown): RemoteControlSettings;
164
+ rotateToken(): RemoteControlSettings;
165
+ setDshTheme(theme: string): RemoteControlSettings;
166
+ setDshLocale(locale: string): RemoteControlSettings;
167
+ }
168
+
169
+ function dataDirectory(): string {
170
+ if (process.env.DSH_PROFILE_DIR) return path.join(process.env.DSH_PROFILE_DIR, 'remote-control');
171
+ if (process.env.DSH_HOME) return path.join(process.env.DSH_HOME, 'remote-control');
172
+ return path.join(os.homedir(), '.dsh', 'remote-control');
173
+ }
174
+
175
+ function token(): string {
176
+ return crypto.randomBytes(24).toString('base64url');
177
+ }
178
+
179
+ function oneOf<T extends string>(value: unknown, allowed: readonly T[]): value is T {
180
+ return typeof value === 'string' && (allowed as readonly string[]).includes(value);
181
+ }
182
+
183
+ export function defaultSettings(): RemoteControlSettings {
184
+ const now = Date.now();
185
+ return {
186
+ version: SETTINGS_VERSION,
187
+ token: token(),
188
+ tokenRotatedAt: now,
189
+ tokenEnabled: true,
190
+ allowedInterfaces: null,
191
+ theme: 'dsh',
192
+ dshTheme: 'dark',
193
+ language: 'dsh',
194
+ dshLocale: '',
195
+ temperatureUnit: 'celsius',
196
+ weather: {
197
+ mode: 'auto',
198
+ name: '',
199
+ latitude: null,
200
+ longitude: null,
201
+ forecastMode: 'hourly',
202
+ },
203
+ balance: {
204
+ spendWindow: 'calendar',
205
+ lowThreshold: null,
206
+ showPeakValley: true,
207
+ },
208
+ notifications: {
209
+ enabledApps: [],
210
+ limit: 5,
211
+ pushQuestions: false,
212
+ },
213
+ tls: {
214
+ enabled: false,
215
+ certPath: '',
216
+ keyPath: '',
217
+ },
218
+ navPosition: 'left',
219
+ pomodoro: {
220
+ enabled: false,
221
+ focusMinutes: 25,
222
+ breakMinutes: 5,
223
+ soundPath: '',
224
+ loopAlarm: false,
225
+ },
226
+ countdown: {
227
+ enabled: false,
228
+ presets: [...DEFAULT_COUNTDOWN_PRESETS],
229
+ },
230
+ pageOrder: ['home', 'sessions', 'music', 'performance'],
231
+ pageEnabled: { home: true, sessions: true, music: true, performance: true },
232
+ updatedAt: now,
233
+ };
234
+ }
235
+
236
+ function normalizeStringArray(value: unknown): string[] {
237
+ if (!Array.isArray(value)) return [];
238
+ return [...new Set(value.filter((item): item is string => typeof item === 'string' && item.length > 0).slice(0, 100))];
239
+ }
240
+
241
+ /**
242
+ * Keep one to five preset durations, dropping blank/invalid slots so a user
243
+ * who saved two or three presets keeps exactly that many. A missing or fully
244
+ * empty list falls back to the defaults instead of leaving the countdown with
245
+ * nothing to select.
246
+ */
247
+ function normalizeCountdownPresets(value: unknown): number[] {
248
+ if (!Array.isArray(value)) return [...DEFAULT_COUNTDOWN_PRESETS];
249
+ const presets: number[] = [];
250
+ for (const entry of value.slice(0, COUNTDOWN_PRESET_LIMIT)) {
251
+ if (entry === null || entry === undefined || entry === '') continue;
252
+ const seconds = Math.round(Number(entry));
253
+ if (!Number.isFinite(seconds)) continue;
254
+ presets.push(Math.max(COUNTDOWN_MIN_SECONDS, Math.min(COUNTDOWN_MAX_SECONDS, seconds)));
255
+ }
256
+ return presets.length > 0 ? presets : [...DEFAULT_COUNTDOWN_PRESETS];
257
+ }
258
+
259
+ /** Round a minute value to two decimals and clamp it to the accepted range. */
260
+ function normalizeMinutes(value: unknown, min: number, max: number, fallback: number): number {
261
+ const minutes = Number(value);
262
+ if (!Number.isFinite(minutes)) return fallback;
263
+ return Math.max(min, Math.min(max, Math.round(minutes * 100) / 100));
264
+ }
265
+
266
+ function normalize(raw: unknown): RemoteControlSettings {
267
+ const base = defaultSettings();
268
+ const input: JsonRecord = isRecord(raw) ? raw : {};
269
+ const weather: JsonRecord = isRecord(input.weather) ? input.weather : {};
270
+ const balance: JsonRecord = isRecord(input.balance) ? input.balance : {};
271
+ const notifications: JsonRecord = isRecord(input.notifications) ? input.notifications : {};
272
+ const tls: JsonRecord = isRecord(input.tls) ? input.tls : {};
273
+ const pomodoro: JsonRecord = isRecord(input.pomodoro) ? input.pomodoro : {};
274
+ const countdown: JsonRecord = isRecord(input.countdown) ? input.countdown : {};
275
+ const latitudeInput = weather.latitude;
276
+ const longitudeInput = weather.longitude;
277
+ const latitude = latitudeInput === null || latitudeInput === undefined || latitudeInput === '' ? NaN : Number(latitudeInput);
278
+ const longitude = longitudeInput === null || longitudeInput === undefined || longitudeInput === '' ? NaN : Number(longitudeInput);
279
+ const mode = weather.mode === 'manual' ? 'manual' : 'auto';
280
+ const zeroPair = latitude === 0 && longitude === 0;
281
+ const validLatitude = Number.isFinite(latitude) && latitude >= -90 && latitude <= 90 ? latitude : null;
282
+ const validLongitude = Number.isFinite(longitude) && longitude >= -180 && longitude <= 180 ? longitude : null;
283
+ const normalizedLatitude = mode === 'auto' && zeroPair ? null : validLatitude;
284
+ const normalizedLongitude = mode === 'auto' && zeroPair ? null : validLongitude;
285
+ const lowThreshold = Number(balance.lowThreshold);
286
+ const themeInput = input.theme;
287
+ const dshThemeInput = input.dshTheme;
288
+ const theme: ThemeSetting = oneOf(themeInput, ['dsh', 'light', 'dark'] as const) ? themeInput : 'dsh';
289
+ const dshTheme: DshTheme = oneOf(dshThemeInput, ['light', 'dark'] as const) ? dshThemeInput : 'dark';
290
+ const languageInput = input.language;
291
+ const language: LanguageSetting = oneOf(languageInput, ['dsh', 'zh', 'en'] as const) ? languageInput : 'dsh';
292
+ const navPositionInput = input.navPosition;
293
+ const navPosition: NavPosition = oneOf(navPositionInput, ['left', 'right', 'top', 'bottom'] as const)
294
+ ? navPositionInput
295
+ : 'left';
296
+ const pomodoroSettings: PomodoroSettings = {
297
+ enabled: pomodoro.enabled === true,
298
+ focusMinutes: normalizeMinutes(pomodoro.focusMinutes, POMODORO_MIN_MINUTES, POMODORO_MAX_FOCUS_MINUTES, 25),
299
+ breakMinutes: normalizeMinutes(pomodoro.breakMinutes, POMODORO_MIN_MINUTES, POMODORO_MAX_BREAK_MINUTES, 5),
300
+ soundPath: typeof pomodoro.soundPath === 'string' ? pomodoro.soundPath.slice(0, 2048) : '',
301
+ loopAlarm: pomodoro.loopAlarm === true,
302
+ };
303
+ const countdownSettings: CountdownSettings = {
304
+ enabled: countdown.enabled === true,
305
+ presets: normalizeCountdownPresets(countdown.presets),
306
+ };
307
+ // Only one timer may be enabled; a hand-edited file that turns both on keeps
308
+ // the pomodoro so existing profiles never change behavior silently.
309
+ if (pomodoroSettings.enabled) countdownSettings.enabled = false;
310
+ const requestedOrder = Array.isArray(input.pageOrder)
311
+ ? input.pageOrder.filter((key): key is PageKey => typeof key === 'string' && (PAGE_KEYS as readonly string[]).includes(key))
312
+ : [];
313
+ const pageOrder: PageKey[] = [...new Set(requestedOrder)];
314
+ for (const key of PAGE_KEYS) if (!pageOrder.includes(key)) pageOrder.push(key);
315
+ const normalizedOrder = pageOrder.slice(0, PAGE_KEYS.length);
316
+ const pageEnabledInput: JsonRecord = isRecord(input.pageEnabled) ? input.pageEnabled : {};
317
+ const pageEnabled = Object.fromEntries(PAGE_KEYS.map((key) => [key, pageEnabledInput[key] !== false])) as Record<
318
+ PageKey,
319
+ boolean
320
+ >;
321
+ if (!Object.values(pageEnabled).some(Boolean)) pageEnabled.home = true;
322
+
323
+ return {
324
+ version: SETTINGS_VERSION,
325
+ token: typeof input.token === 'string' && input.token.length >= 16 ? input.token : base.token,
326
+ tokenRotatedAt: Number.isFinite(Number(input.tokenRotatedAt)) ? Number(input.tokenRotatedAt) : base.tokenRotatedAt,
327
+ tokenEnabled: input.tokenEnabled !== false,
328
+ allowedInterfaces: input.allowedInterfaces === null ? null : normalizeStringArray(input.allowedInterfaces),
329
+ theme,
330
+ dshTheme,
331
+ language,
332
+ dshLocale: normalizeDshLocale(input.dshLocale),
333
+ temperatureUnit: input.temperatureUnit === 'fahrenheit' ? 'fahrenheit' : 'celsius',
334
+ weather: {
335
+ mode,
336
+ name: typeof weather.name === 'string' ? weather.name.slice(0, 80) : '',
337
+ latitude: normalizedLatitude,
338
+ longitude: normalizedLongitude,
339
+ forecastMode: weather.forecastMode === 'daily' ? 'daily' : 'hourly',
340
+ },
341
+ balance: {
342
+ spendWindow: balance.spendWindow === 'rolling' ? 'rolling' : 'calendar',
343
+ lowThreshold:
344
+ balance.lowThreshold === null || balance.lowThreshold === ''
345
+ ? null
346
+ : Number.isFinite(lowThreshold) && lowThreshold >= 0
347
+ ? lowThreshold
348
+ : null,
349
+ showPeakValley: balance.showPeakValley !== false,
350
+ },
351
+ notifications: {
352
+ enabledApps: normalizeStringArray(notifications.enabledApps),
353
+ limit: Math.max(1, Math.min(20, Number.isFinite(Number(notifications.limit)) ? Math.round(Number(notifications.limit)) : 5)),
354
+ pushQuestions: notifications.pushQuestions === true,
355
+ },
356
+ tls: {
357
+ enabled: tls.enabled === true,
358
+ certPath: typeof tls.certPath === 'string' ? tls.certPath.slice(0, 4096) : '',
359
+ keyPath: typeof tls.keyPath === 'string' ? tls.keyPath.slice(0, 4096) : '',
360
+ },
361
+ navPosition,
362
+ pomodoro: pomodoroSettings,
363
+ countdown: countdownSettings,
364
+ pageOrder: normalizedOrder,
365
+ pageEnabled,
366
+ updatedAt: Number.isFinite(Number(input.updatedAt)) ? Number(input.updatedAt) : Date.now(),
367
+ };
368
+ }
369
+
370
+ export function createSettingsStore(): SettingsStore {
371
+ const dir = dataDirectory();
372
+ const file = path.join(dir, 'settings.json');
373
+ fs.mkdirSync(dir, { recursive: true });
374
+
375
+ let current: RemoteControlSettings;
376
+ try {
377
+ current = normalize(JSON.parse(fs.readFileSync(file, 'utf8')) as unknown);
378
+ } catch {
379
+ current = defaultSettings();
380
+ }
381
+ fs.writeFileSync(file, JSON.stringify(current, null, 2), 'utf8');
382
+
383
+ function save(next: unknown): RemoteControlSettings {
384
+ current = normalize({ ...(isRecord(next) ? next : {}), updatedAt: Date.now() });
385
+ const temporary = `${file}.tmp-${process.pid}`;
386
+ fs.writeFileSync(temporary, JSON.stringify(current, null, 2), 'utf8');
387
+ fs.renameSync(temporary, file);
388
+ return current;
389
+ }
390
+
391
+ function update(patch: unknown): RemoteControlSettings {
392
+ const source: JsonRecord = isRecord(patch) ? patch : {};
393
+ const next: JsonRecord = {
394
+ ...current,
395
+ ...source,
396
+ weather: { ...current.weather, ...(isRecord(source.weather) ? source.weather : {}) },
397
+ balance: { ...current.balance, ...(isRecord(source.balance) ? source.balance : {}) },
398
+ notifications: {
399
+ ...current.notifications,
400
+ ...(isRecord(source.notifications) ? source.notifications : {}),
401
+ },
402
+ tls: { ...current.tls, ...(isRecord(source.tls) ? source.tls : {}) },
403
+ pomodoro: { ...current.pomodoro, ...(isRecord(source.pomodoro) ? source.pomodoro : {}) },
404
+ countdown: { ...current.countdown, ...(isRecord(source.countdown) ? source.countdown : {}) },
405
+ };
406
+ // Respect whichever timer the patch turns on, even when the caller sends
407
+ // only one side of the pair; normalize() is the final authority.
408
+ const pomodoroPatch = isRecord(source.pomodoro) ? source.pomodoro : {};
409
+ const countdownPatch = isRecord(source.countdown) ? source.countdown : {};
410
+ if (pomodoroPatch.enabled === true && countdownPatch.enabled !== true) {
411
+ next.countdown = { ...(isRecord(next.countdown) ? next.countdown : {}), enabled: false };
412
+ } else if (countdownPatch.enabled === true && pomodoroPatch.enabled !== true) {
413
+ next.pomodoro = { ...(isRecord(next.pomodoro) ? next.pomodoro : {}), enabled: false };
414
+ }
415
+ return save(next);
416
+ }
417
+
418
+ function rotateToken(): RemoteControlSettings {
419
+ return save({ ...current, token: token(), tokenRotatedAt: Date.now() });
420
+ }
421
+
422
+ function setDshTheme(theme: string): RemoteControlSettings {
423
+ if (theme !== 'light' && theme !== 'dark') return current;
424
+ if (current.dshTheme === theme) return current;
425
+ return save({ ...current, dshTheme: theme });
426
+ }
427
+
428
+ function setDshLocale(locale: string): RemoteControlSettings {
429
+ const next = normalizeDshLocale(locale);
430
+ if (!next || current.dshLocale === next) return current;
431
+ return save({ ...current, dshLocale: next });
432
+ }
433
+
434
+ function publicSettings(): PublicSettings {
435
+ return {
436
+ tokenEnabled: current.tokenEnabled,
437
+ tokenRotatedAt: current.tokenRotatedAt,
438
+ allowedInterfaces: current.allowedInterfaces,
439
+ theme: current.theme,
440
+ effectiveTheme: current.theme === 'dsh' ? current.dshTheme : current.theme,
441
+ language: current.language,
442
+ locale: resolveLocale(current.language, current.dshLocale),
443
+ temperatureUnit: current.temperatureUnit,
444
+ weather: { ...current.weather },
445
+ balance: { ...current.balance },
446
+ notifications: { ...current.notifications, enabledApps: [...current.notifications.enabledApps] },
447
+ tls: { ...current.tls },
448
+ navPosition: current.navPosition,
449
+ pomodoro: { ...current.pomodoro },
450
+ countdown: { ...current.countdown, presets: [...current.countdown.presets] },
451
+ pageOrder: [...current.pageOrder],
452
+ pageEnabled: { ...current.pageEnabled },
453
+ updatedAt: current.updatedAt,
454
+ };
455
+ }
456
+
457
+ return {
458
+ dir,
459
+ file,
460
+ get: () => current,
461
+ public: publicSettings,
462
+ update,
463
+ save,
464
+ rotateToken,
465
+ setDshTheme,
466
+ setDshLocale,
467
+ };
468
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Shared Host-side contracts for the remote console.
3
+ *
4
+ * These types describe the small part of the DSH Host runtime this plugin
5
+ * touches, plus the payloads that cross module boundaries (probe results,
6
+ * media snapshots, notifications, WebSocket frames). They are deliberately
7
+ * structural: DSH services are resolved by name through `ctx.get`, so every
8
+ * consumer validates the shape it needs at runtime.
9
+ */
10
+
11
+ import type { IncomingMessage, Server as HttpServer } from 'node:http';
12
+ import type { Server as HttpsServer } from 'node:https';
13
+
14
+ /** Minimal logger sink the Host may expose through `ctx.logger`. */
15
+ export interface DshLogger {
16
+ info(message: string): void;
17
+ warn?(message: string): void;
18
+ error?(message: string): void;
19
+ }
20
+
21
+ export type DshLoggerFactory = (id: string) => DshLogger;
22
+
23
+ /** A DSH lifecycle hook handler (`user-questions/request`, `approval/request`). */
24
+ export type DshEventHandler = (request: unknown, next: () => Promise<unknown>) => Promise<unknown>;
25
+
26
+ /** Registration options accepted by the Cordis event bus. */
27
+ export interface DshEventListenerOptions {
28
+ /** Run this listener before listeners registered earlier on the same event. */
29
+ prepend?: boolean;
30
+ }
31
+
32
+ /**
33
+ * The subset of the Cordis plugin context this plugin uses. Everything is
34
+ * optional so the service can also run against a minimal composition in tests.
35
+ */
36
+ export interface DshContext {
37
+ logger?: DshLoggerFactory | DshLogger;
38
+ get?: (name: string) => unknown;
39
+ on?: (
40
+ event: string,
41
+ handler: DshEventHandler,
42
+ options?: DshEventListenerOptions | boolean,
43
+ ) => (() => void) | void;
44
+ effect?: (fn: () => (() => void) | void, label?: string) => unknown;
45
+ }
46
+
47
+ /** One connected phone/browser as tracked by the WebSocket hub. */
48
+ export interface WebSocketClient {
49
+ readonly id: string;
50
+ readonly ip: string;
51
+ readonly userAgent: string;
52
+ readonly label: string;
53
+ readonly connectedAt: number;
54
+ lastSeen: number;
55
+ send(value: unknown): void;
56
+ close(code?: number, reason?: string): void;
57
+ }
58
+
59
+ export interface WebSocketClientInfo {
60
+ id: string;
61
+ label: string;
62
+ ip: string;
63
+ connectedAt: number;
64
+ lastSeen: number;
65
+ }
66
+
67
+ export interface WebSocketAuthorizeDecision {
68
+ ok: boolean;
69
+ status?: number;
70
+ }
71
+
72
+ export interface WebSocketHubOptions {
73
+ server: HttpServer | HttpsServer;
74
+ path?: string;
75
+ authorize?: (req: IncomingMessage, url: URL) => WebSocketAuthorizeDecision | undefined;
76
+ onConnection?: (client: WebSocketClient) => void;
77
+ onMessage?: (client: WebSocketClient, message: unknown) => void;
78
+ onClose?: (client: WebSocketClient) => void;
79
+ }
80
+
81
+ export interface WebSocketHub {
82
+ broadcast(value: unknown, predicate?: ((client: WebSocketClient) => boolean) | null): void;
83
+ closeAll(reason?: string): void;
84
+ close(id: string, reason?: string): void;
85
+ info(): WebSocketClientInfo[];
86
+ size(): number;
87
+ send(client: WebSocketClient, value: unknown): void;
88
+ dispose(): void;
89
+ }
90
+
91
+ /** Loose JSON object used while normalising untrusted payloads. */
92
+ export type JsonRecord = Record<string, unknown>;
93
+
94
+ export function isRecord(value: unknown): value is JsonRecord {
95
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
96
+ }
97
+
98
+ /** Raw `dsh-probe media read` payload (already enriched by Rust). */
99
+ export interface ProbeMedia extends JsonRecord {
100
+ appId?: string;
101
+ title?: string;
102
+ artist?: string;
103
+ album?: string;
104
+ albumArtist?: string;
105
+ playbackStatus?: string;
106
+ autoRepeatMode?: string;
107
+ shuffleActive?: boolean;
108
+ position?: number | string | null;
109
+ startTime?: number | string | null;
110
+ endTime?: number | string | null;
111
+ minSeekTime?: number | string | null;
112
+ maxSeekTime?: number | string | null;
113
+ lastUpdatedAt?: number | string | null;
114
+ canPlayPause?: boolean;
115
+ canNext?: boolean;
116
+ canPrevious?: boolean;
117
+ canSeek?: boolean;
118
+ canLike?: boolean;
119
+ canSetLiked?: boolean;
120
+ thumbnail?: string | null;
121
+ thumbnailType?: string | null;
122
+ thumbnailUrl?: string | null;
123
+ liked?: boolean | null;
124
+ trackId?: string | number | null;
125
+ estimatedDuration?: boolean;
126
+ /** `system` (SMTC/MPRIS/MediaRemote), `private` or `accessibility`. */
127
+ provider?: string;
128
+ providerDetail?: string | null;
129
+ }
130
+
131
+ /** Normalised media metadata handed to the browser. */
132
+ export interface PublicMedia {
133
+ appId: string;
134
+ /** Display name resolved from the plugin's application mapping. */
135
+ appName: string;
136
+ title: string;
137
+ artist: string;
138
+ album: string;
139
+ albumArtist: string;
140
+ playbackStatus: string;
141
+ autoRepeatMode: string;
142
+ shuffleActive: boolean;
143
+ position: number;
144
+ startTime: number;
145
+ endTime: number;
146
+ minSeekTime: number;
147
+ maxSeekTime: number;
148
+ lastUpdatedAt: number | null;
149
+ canPlayPause: boolean;
150
+ canNext: boolean;
151
+ canPrevious: boolean;
152
+ canSeek: boolean;
153
+ /** Which layer supplied the media payload. */
154
+ provider: 'system' | 'private' | 'accessibility';
155
+ providerDetail: string | null;
156
+ thumbnail: string | null;
157
+ thumbnailUrl: string | null;
158
+ thumbnailType: string | null;
159
+ liked: boolean | null;
160
+ canLike: boolean;
161
+ canSetLiked: boolean;
162
+ trackId: string | number | null;
163
+ estimatedDuration: boolean;
164
+ sampledAt: number;
165
+ positionReported?: boolean;
166
+ timelineSource?: 'smtc' | 'private' | 'accessibility' | 'none';
167
+ }
168
+
169
+ /** Raw probe envelope: `{ ok, media, sampledAt }` plus backend-specific extras. */
170
+ export interface ProbeMediaResult extends JsonRecord {
171
+ media?: ProbeMedia | null;
172
+ sampledAt?: number | string | null;
173
+ available?: boolean;
174
+ }
175
+
176
+ export interface ProbeVolumeResult extends JsonRecord {
177
+ volume?: number | string | null;
178
+ muted?: boolean;
179
+ }
180
+
181
+ export interface ProbeLocationResult extends JsonRecord {
182
+ latitude?: number | string | null;
183
+ longitude?: number | string | null;
184
+ accuracy?: number | string | null;
185
+ source?: string;
186
+ }
187
+
188
+ /** One OS notification as reported by `dsh-probe notifications`. */
189
+ export interface NotificationItem {
190
+ id?: string;
191
+ appKey?: string;
192
+ appName?: string;
193
+ title?: string;
194
+ body?: string;
195
+ avatar?: string | null;
196
+ hasImage?: boolean;
197
+ receivedAt?: number;
198
+ isGroup?: boolean;
199
+ [key: string]: unknown;
200
+ }
201
+
202
+ /** Aggregated per-application notification counts. */
203
+ export interface NotificationApp {
204
+ key: string;
205
+ name: string;
206
+ count: number;
207
+ [key: string]: unknown;
208
+ }
209
+
210
+ /** Raw `dsh-probe notifications` snapshot. */
211
+ export interface NotificationPayload extends JsonRecord {
212
+ ok?: boolean;
213
+ available?: boolean;
214
+ reason?: string | null;
215
+ source?: string | null;
216
+ liveOnly?: boolean;
217
+ notifications?: NotificationItem[];
218
+ apps?: NotificationApp[];
219
+ updatedAt?: number;
220
+ }
221
+
222
+ /** Browser-facing notification view after app filtering. */
223
+ export interface NotificationView {
224
+ available: boolean;
225
+ reason: string | null;
226
+ liveOnly: boolean;
227
+ notifications: NotificationItem[];
228
+ apps: NotificationApp[];
229
+ enabledApps: string[];
230
+ updatedAt: number;
231
+ }