@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,483 @@
1
+ /**
2
+ * The settings panel itself: data loading, staged edits, immediate actions,
3
+ * pointer-based page reordering, and the DSH settings-style shell.
4
+ *
5
+ * Section markup lives in `sections/*` and receives the small `PanelApi` face
6
+ * assembled here, so adding a section does not require touching this state
7
+ * machine and changing this file does not require reading every section.
8
+ */
9
+
10
+ import * as React from 'react';
11
+ import {
12
+ APP_NAMES_URL,
13
+ DISCONNECT_URL,
14
+ LOCATION_URL,
15
+ SETTINGS_URL,
16
+ TOKEN_URL,
17
+ describeError,
18
+ isRecord,
19
+ isSettingsPayload,
20
+ postJson,
21
+ readClients,
22
+ requestJson,
23
+ } from './api.js';
24
+ import { setPluginLanguage, useTranslator } from './i18n.js';
25
+ import {
26
+ EMPTY_DRAFT,
27
+ PAGE_ORDER,
28
+ buildSavePatch,
29
+ buildSettingsView,
30
+ createDraft,
31
+ locationHint,
32
+ mergeStaged,
33
+ normalizePageOrder,
34
+ pageLabels,
35
+ } from './logic.js';
36
+ import { Btn, h } from './ui.js';
37
+ import { MenuIcon } from './icons.js';
38
+ import { CSS } from './styles.js';
39
+ import { renderAppMapping } from './sections/apps.js';
40
+ import { renderBalance } from './sections/balance.js';
41
+ import { renderConnection } from './sections/connection.js';
42
+ import { renderNotifications } from './sections/notifications.js';
43
+ import { renderPomodoro } from './sections/pomodoro.js';
44
+ import { renderPreferences } from './sections/preferences.js';
45
+ import { renderSystem } from './sections/system.js';
46
+ import { renderWeather } from './sections/weather.js';
47
+ import type {
48
+ DraftState,
49
+ LocationResult,
50
+ PanelApi,
51
+ RemoteApp,
52
+ RemoteClient,
53
+ RemoteInfo,
54
+ RemoteSettings,
55
+ SavePatch,
56
+ SettingsPayload,
57
+ StagedSettings,
58
+ } from './types.js';
59
+
60
+ const { useEffect, useState, useCallback, useRef } = React;
61
+
62
+ type SectionKey = 'connection' | 'preferences' | 'pomodoro' | 'weather' | 'balance' | 'notifications' | 'apps' | 'system';
63
+
64
+ const MENU: Array<{ key: SectionKey; labelKey: string; icon: string }> = [
65
+ { key: 'connection', labelKey: 'connection', icon: 'link' },
66
+ { key: 'preferences', labelKey: 'menuPlugin', icon: 'sliders' },
67
+ { key: 'pomodoro', labelKey: 'menuPomodoro', icon: 'timer' },
68
+ { key: 'weather', labelKey: 'menuWeather', icon: 'cloud' },
69
+ { key: 'balance', labelKey: 'menuBalance', icon: 'wallet' },
70
+ { key: 'notifications', labelKey: 'menuNotifications', icon: 'bell' },
71
+ { key: 'apps', labelKey: 'menuAppMapping', icon: 'apps' },
72
+ { key: 'system', labelKey: 'menuSystem', icon: 'info' },
73
+ ];
74
+
75
+ const NARROW_QUERY = '(max-width: 767px)';
76
+
77
+ /** Follow the same breakpoint as the stylesheet: the rail becomes a selector. */
78
+ function useNarrowLayout(): boolean {
79
+ const [narrow, setNarrow] = useState(() => (
80
+ typeof window !== 'undefined' && typeof window.matchMedia === 'function'
81
+ ? window.matchMedia(NARROW_QUERY).matches
82
+ : false
83
+ ));
84
+ useEffect(() => {
85
+ if (typeof window.matchMedia !== 'function') return;
86
+ const query = window.matchMedia(NARROW_QUERY);
87
+ const update = (): void => setNarrow(query.matches);
88
+ update();
89
+ query.addEventListener?.('change', update);
90
+ return () => query.removeEventListener?.('change', update);
91
+ }, []);
92
+ return narrow;
93
+ }
94
+
95
+ function selectAllNames(payload: SettingsPayload): string[] {
96
+ return (payload.interfaces ?? []).map((entry) => entry.name);
97
+ }
98
+
99
+ function disabledPages(order: string[], values: Record<string, boolean>): boolean {
100
+ return order.every((key) => !values[key]);
101
+ }
102
+
103
+ export function RemotePanel(): React.ReactElement {
104
+ const t = useTranslator();
105
+ const narrow = useNarrowLayout();
106
+ const [data, setData] = useState<SettingsPayload | null>(null);
107
+ const [loading, setLoading] = useState(true);
108
+ const [busy, setBusy] = useState(false);
109
+ const [error, setError] = useState('');
110
+ const [notice, setNotice] = useState('');
111
+ const [copied, setCopied] = useState(false);
112
+ const [section, setSection] = useState<SectionKey>('connection');
113
+ const [draft, setDraft] = useState<DraftState>(EMPTY_DRAFT);
114
+ const [selectedInterfaces, setSelectedInterfaces] = useState<string[]>([]);
115
+ const [selectedApps, setSelectedApps] = useState<string[]>([]);
116
+ const [appNames, setAppNames] = useState<Record<string, string>>({});
117
+ const [appBusyKey, setAppBusyKey] = useState('');
118
+ const [locationBusy, setLocationBusy] = useState(false);
119
+ const [locationResult, setLocationResult] = useState<LocationResult | null>(null);
120
+ const [staged, setStaged] = useState<StagedSettings>({});
121
+ const [draggingKey, setDraggingKey] = useState<string | null>(null);
122
+ const [dragIndex, setDragIndex] = useState<number | null>(null);
123
+ const orderListRef = useRef<HTMLDivElement | null>(null);
124
+ const dragCleanupRef = useRef<(() => void) | null>(null);
125
+ /** Latest merged settings, used by pointer handlers that outlive a render. */
126
+ const settingsRef = useRef<RemoteSettings | null>(null);
127
+
128
+ useEffect(() => () => {
129
+ dragCleanupRef.current?.();
130
+ dragCleanupRef.current = null;
131
+ }, []);
132
+
133
+ const hydrate = useCallback((payload: unknown): void => {
134
+ if (!isSettingsPayload(payload)) return;
135
+ setData(payload);
136
+ setStaged({});
137
+ // The saved plugin language drives every label in this panel; apply it
138
+ // before the next render so the new language paints in one pass.
139
+ setPluginLanguage(payload.settings.language);
140
+ const names = selectAllNames(payload);
141
+ setSelectedInterfaces(payload.settings.allowedInterfaces === null ? names : [...payload.settings.allowedInterfaces]);
142
+ const appKeys = (payload.apps ?? []).map((app) => app.key);
143
+ const enabled = payload.settings.notifications?.enabledApps ?? [];
144
+ setSelectedApps(enabled.length ? enabled.filter((key) => appKeys.includes(key)) : appKeys);
145
+ setAppNames(Object.fromEntries((payload.apps ?? []).map((app) => [app.key, app.name])));
146
+ setDraft(createDraft(payload.settings));
147
+ }, []);
148
+
149
+ const load = useCallback(async (): Promise<void> => {
150
+ setLoading(true);
151
+ setError('');
152
+ try {
153
+ hydrate(await requestJson(SETTINGS_URL));
154
+ } catch (err) {
155
+ setError(describeError(err));
156
+ } finally {
157
+ setLoading(false);
158
+ }
159
+ }, [hydrate]);
160
+
161
+ const save = useCallback(async (patch: SavePatch): Promise<void> => {
162
+ setBusy(true);
163
+ setError('');
164
+ try {
165
+ hydrate(await postJson(SETTINGS_URL, patch));
166
+ setNotice(t('saved'));
167
+ window.setTimeout(() => setNotice(''), 1600);
168
+ } catch (err) {
169
+ setError(describeError(err));
170
+ } finally {
171
+ setBusy(false);
172
+ }
173
+ }, [hydrate, t]);
174
+
175
+ const stage = useCallback((patch: StagedSettings): void => {
176
+ setStaged((current) => mergeStaged(current, patch));
177
+ }, []);
178
+
179
+ useEffect(() => { void load(); }, [load]);
180
+
181
+ const copy = async (): Promise<void> => {
182
+ const url = data?.info?.routes?.remote || '';
183
+ if (!url) return;
184
+ try {
185
+ await navigator.clipboard.writeText(url);
186
+ setCopied(true);
187
+ window.setTimeout(() => setCopied(false), 1600);
188
+ } catch {
189
+ setError(t('error'));
190
+ }
191
+ };
192
+
193
+ const rotate = async (): Promise<void> => {
194
+ setBusy(true);
195
+ setError('');
196
+ try {
197
+ hydrate(await postJson(TOKEN_URL));
198
+ setNotice(t('saved'));
199
+ window.setTimeout(() => setNotice(''), 1600);
200
+ } catch (err) {
201
+ setError(describeError(err));
202
+ } finally {
203
+ setBusy(false);
204
+ }
205
+ };
206
+
207
+ const saveAppName = async (key: string, name: string): Promise<void> => {
208
+ setAppBusyKey(key);
209
+ setError('');
210
+ try {
211
+ hydrate(await postJson(APP_NAMES_URL, { key, name }));
212
+ setNotice(t('saved'));
213
+ window.setTimeout(() => setNotice(''), 1600);
214
+ } catch (err) {
215
+ setError(describeError(err));
216
+ } finally {
217
+ setAppBusyKey('');
218
+ }
219
+ };
220
+
221
+ const requestLocation = async (): Promise<void> => {
222
+ setLocationBusy(true);
223
+ setError('');
224
+ try {
225
+ const payload = await postJson(LOCATION_URL, { openSettings: true });
226
+ const record = isRecord(payload) ? payload : {};
227
+ const result: LocationResult = {
228
+ allowed: record.allowed === true,
229
+ access: String(record.access || 'unknown'),
230
+ reason: typeof record.reason === 'string' ? record.reason : null,
231
+ lowIntegrity: record.lowIntegrity === true,
232
+ openedSettings: record.openedSettings === true,
233
+ };
234
+ setLocationResult(result);
235
+ setNotice(locationHint(t, result));
236
+ window.setTimeout(() => setNotice(''), 2600);
237
+ } catch (err) {
238
+ setError(describeError(err));
239
+ } finally {
240
+ setLocationBusy(false);
241
+ }
242
+ };
243
+
244
+ const disconnect = async (id: string): Promise<void> => {
245
+ try {
246
+ const payload = await postJson(DISCONNECT_URL, { id });
247
+ const clients = readClients(payload);
248
+ setData((current) => (current ? { ...current, clients } : current));
249
+ } catch (err) {
250
+ setError(describeError(err));
251
+ }
252
+ };
253
+
254
+ const toggleApp = (key: string): void => {
255
+ const next = new Set(selectedApps);
256
+ if (next.has(key)) next.delete(key); else next.add(key);
257
+ if (next.size === 0) {
258
+ setNotice(t('keepOneApp'));
259
+ window.setTimeout(() => setNotice(''), 1600);
260
+ return;
261
+ }
262
+ setSelectedApps([...next]);
263
+ };
264
+
265
+ const selectAllApps = (): void => {
266
+ setSelectedApps((data?.apps ?? []).map((app) => app.key));
267
+ };
268
+
269
+ const toggleInterface = (name: string): void => {
270
+ setSelectedInterfaces((current) => current.includes(name) ? current.filter((item) => item !== name) : [...current, name]);
271
+ };
272
+
273
+ const togglePage = (key: string): void => {
274
+ const current: Record<string, boolean> = settingsRef.current?.pageEnabled ?? {};
275
+ const next: Record<string, boolean> = { ...current, [key]: !current[key] };
276
+ if (disabledPages(PAGE_ORDER, next)) {
277
+ setNotice(t('keepOnePage'));
278
+ window.setTimeout(() => setNotice(''), 1600);
279
+ return;
280
+ }
281
+ stage({ pageEnabled: next });
282
+ };
283
+
284
+ const computeInsertIndex = (clientY: number): number => {
285
+ const list = orderListRef.current;
286
+ if (!list) return 0;
287
+ const rows = [...list.querySelectorAll<HTMLElement>('.drc-order-row:not(.is-dragging)')];
288
+ for (let index = 0; index < rows.length; index += 1) {
289
+ const rect = rows[index].getBoundingClientRect();
290
+ if (clientY < rect.top + rect.height / 2) return index;
291
+ }
292
+ return rows.length;
293
+ };
294
+
295
+ const startOrderDrag = (key: string, event: React.PointerEvent<HTMLElement>): void => {
296
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
297
+ const dragTarget = event.target;
298
+ if (dragTarget instanceof Element && dragTarget.closest('button, input, a, select, textarea')) return;
299
+ event.preventDefault();
300
+ const pointerId = event.pointerId;
301
+ const startX = event.clientX;
302
+ const startY = event.clientY;
303
+ const order = settingsRef.current ? normalizePageOrder(settingsRef.current.pageOrder ?? PAGE_ORDER) : PAGE_ORDER;
304
+ const originalIndex = order.indexOf(key);
305
+ let started = false;
306
+
307
+ const finish = (upEvent: PointerEvent): void => {
308
+ if (upEvent.pointerId !== pointerId) return;
309
+ window.removeEventListener('pointermove', onMove);
310
+ window.removeEventListener('pointerup', finish);
311
+ window.removeEventListener('pointercancel', finish);
312
+ document.body.classList.remove('drc-sorting');
313
+ dragCleanupRef.current = null;
314
+ if (!started || upEvent.type === 'pointercancel') {
315
+ setDraggingKey(null);
316
+ setDragIndex(null);
317
+ return;
318
+ }
319
+ const remaining = order.filter((item) => item !== key);
320
+ const insertAt = Math.max(0, Math.min(computeInsertIndex(upEvent.clientY), remaining.length));
321
+ const next = [...remaining.slice(0, insertAt), key, ...remaining.slice(insertAt)];
322
+ if (next.join('|') !== order.join('|')) stage({ pageOrder: next });
323
+ setDraggingKey(null);
324
+ setDragIndex(null);
325
+ };
326
+
327
+ const onMove = (moveEvent: PointerEvent): void => {
328
+ if (moveEvent.pointerId !== pointerId) return;
329
+ if (!started) {
330
+ if (Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 5) return;
331
+ started = true;
332
+ setDraggingKey(key);
333
+ setDragIndex(originalIndex);
334
+ document.body.classList.add('drc-sorting');
335
+ }
336
+ moveEvent.preventDefault();
337
+ const next = computeInsertIndex(moveEvent.clientY);
338
+ setDragIndex((current) => (current === next ? current : next));
339
+ };
340
+
341
+ window.addEventListener('pointermove', onMove, { passive: false });
342
+ window.addEventListener('pointerup', finish);
343
+ window.addEventListener('pointercancel', finish);
344
+ dragCleanupRef.current = () => {
345
+ window.removeEventListener('pointermove', onMove);
346
+ window.removeEventListener('pointerup', finish);
347
+ window.removeEventListener('pointercancel', finish);
348
+ document.body.classList.remove('drc-sorting');
349
+ };
350
+ };
351
+
352
+ if (loading) {
353
+ return h('div', { className: 'drc-page' },
354
+ h('style', null, CSS),
355
+ h('div', { className: 'drc-empty', style: { padding: 32 } }, t('loading')),
356
+ );
357
+ }
358
+ if (!data) {
359
+ return h('div', { className: 'drc-page' },
360
+ h('style', null, CSS),
361
+ h('div', { className: 'drc-empty', style: { padding: 32, maxWidth: 680 } },
362
+ h('h1', { className: 'drc-section-title' }, t('heading')),
363
+ h('p', { className: 'drc-banner is-error' }, error || t('noWebServer')),
364
+ h(Btn, { primary: true, onClick: () => void load() }, t('refresh')),
365
+ ),
366
+ );
367
+ }
368
+
369
+ const info: RemoteInfo = data.info ?? {};
370
+ const clients: RemoteClient[] = data.clients ?? [];
371
+ const settings = buildSettingsView(data.settings, staged);
372
+ settingsRef.current = settings;
373
+ const order = normalizePageOrder(settings.pageOrder ?? PAGE_ORDER);
374
+ const labels = pageLabels(t);
375
+ const notificationApps: RemoteApp[] = data.apps ?? [];
376
+ const remote = info.routes?.remote || '';
377
+
378
+ const saveAll = (): void => {
379
+ void save(buildSavePatch({
380
+ data,
381
+ settings,
382
+ draft,
383
+ selectedInterfaces,
384
+ selectedApps,
385
+ notificationApps,
386
+ order,
387
+ }));
388
+ };
389
+
390
+ const api: PanelApi = {
391
+ t,
392
+ data,
393
+ settings,
394
+ draft,
395
+ info,
396
+ apps: notificationApps,
397
+ clients,
398
+ busy,
399
+ appBusyKey,
400
+ locationBusy,
401
+ locationResult,
402
+ copied,
403
+ order,
404
+ labels,
405
+ draggingKey,
406
+ dragIndex,
407
+ selectedInterfaces,
408
+ selectedApps,
409
+ appNames,
410
+ orderListRef,
411
+ setDraft,
412
+ setError,
413
+ setAppNames,
414
+ selectAllApps,
415
+ stage,
416
+ save,
417
+ saveAll,
418
+ copy,
419
+ rotate,
420
+ saveAppName,
421
+ requestLocation,
422
+ disconnect,
423
+ toggleApp,
424
+ toggleInterface,
425
+ togglePage,
426
+ startOrderDrag,
427
+ };
428
+
429
+ const content: React.ReactElement = section === 'connection' ? renderConnection(api)
430
+ : section === 'preferences' ? renderPreferences(api)
431
+ : section === 'pomodoro' ? renderPomodoro(api)
432
+ : section === 'weather' ? renderWeather(api)
433
+ : section === 'balance' ? renderBalance(api)
434
+ : section === 'notifications' ? renderNotifications(api)
435
+ : section === 'apps' ? renderAppMapping(api)
436
+ : renderSystem(api);
437
+
438
+ return h('div', { className: 'drc-page' },
439
+ h('style', null, CSS),
440
+ h('div', { className: 'drc-shell' },
441
+ h('div', { className: 'drc-nav-title' }, t('heading')),
442
+ h('header', { className: 'drc-head' },
443
+ h('div', { className: 'drc-head-actions' },
444
+ h('span', { className: 'drc-status' },
445
+ h('i', { className: `drc-dot ${info.running ? 'ok' : 'bad'}` }),
446
+ info.running ? t('serviceRunning') : t('serviceStopped'),
447
+ ),
448
+ h(Btn, { primary: true, onClick: saveAll, disabled: busy }, t('saveSettings')),
449
+ h(Btn, { disabled: !remote, onClick: () => window.open(remote, '_blank', 'noopener,noreferrer') }, t('open')),
450
+ h(Btn, { onClick: () => void load(), disabled: busy }, t('refresh')),
451
+ ),
452
+ ),
453
+ narrow
454
+ ? h('nav', { className: 'drc-nav drc-nav-compact', 'aria-label': t('heading') },
455
+ h('div', { className: 'drc-nav-select-wrap' },
456
+ h('select', {
457
+ className: 'drc-nav-select',
458
+ value: section,
459
+ 'aria-label': t('heading'),
460
+ onChange: (event: React.ChangeEvent<HTMLSelectElement>) => setSection(event.target.value as SectionKey),
461
+ }, MENU.map((item) => h('option', { key: item.key, value: item.key }, t(item.labelKey)))),
462
+ ),
463
+ )
464
+ : h('nav', { className: 'drc-nav', 'aria-label': t('heading') },
465
+ h('div', { className: 'drc-nav-list' }, MENU.map((item) => h('button', {
466
+ key: item.key,
467
+ type: 'button',
468
+ className: `drc-nav-item${section === item.key ? ' is-active' : ''}`,
469
+ 'aria-current': section === item.key ? 'page' : undefined,
470
+ onClick: () => setSection(item.key),
471
+ },
472
+ h(MenuIcon, { name: item.icon }),
473
+ h('span', { className: 'drc-nav-label' }, t(item.labelKey)),
474
+ ))),
475
+ ),
476
+ h('main', { className: 'drc-main' },
477
+ error ? h('div', { className: 'drc-banner is-error' }, `${t('error')}: ${error}`) : null,
478
+ notice ? h('div', { className: 'drc-banner is-ok' }, notice) : null,
479
+ content,
480
+ ),
481
+ ),
482
+ );
483
+ }
@@ -0,0 +1,40 @@
1
+ /** App-name mapping: notification and media display names, saved immediately. */
2
+
3
+ import type * as React from 'react';
4
+ import { Btn, groupHead, h, sectionHead } from '../ui.js';
5
+ import type { PanelApi } from '../types.js';
6
+
7
+ export function renderAppMapping(api: PanelApi): React.ReactElement {
8
+ const { t, data, appNames, setAppNames, appBusyKey, saveAppName } = api;
9
+ const apps = data.apps ?? [];
10
+ return h('div', { className: 'drc-section' },
11
+ sectionHead(t('appMapping'), t('appsPageHint')),
12
+ h('div', { className: 'drc-group' },
13
+ groupHead(t('appNamesBlock'), t('appMappingHint')),
14
+ apps.length === 0
15
+ ? h('p', { className: 'drc-empty' }, t('noApps'))
16
+ : h('div', { className: 'drc-list' }, apps.map((app) => h('div', { className: 'drc-map-row', key: app.key },
17
+ h('div', { className: 'drc-map-main' },
18
+ h('input', {
19
+ className: 'drc-map-input',
20
+ value: appNames[app.key] ?? app.name,
21
+ placeholder: app.defaultName || app.key,
22
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
23
+ const value = event.target.value;
24
+ setAppNames((current) => ({ ...current, [app.key]: value }));
25
+ },
26
+ onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => {
27
+ if (event.key === 'Enter') void saveAppName(app.key, event.currentTarget.value);
28
+ },
29
+ }),
30
+ h('small', { className: 'drc-code drc-map-key', title: app.key }, app.key),
31
+ ),
32
+ h('div', { className: 'drc-map-meta' },
33
+ (app.sources ?? []).map((source) => h('span', { className: 'drc-tag', key: source }, source === 'music' ? t('sourceMusic') : t('sourceNotification'))),
34
+ h(Btn, { small: true, onClick: () => void saveAppName(app.key, appNames[app.key] ?? app.name), disabled: appBusyKey === app.key }, t('save')),
35
+ h(Btn, { small: true, onClick: () => void saveAppName(app.key, ''), disabled: appBusyKey === app.key || !app.customName }, t('resetName')),
36
+ ),
37
+ ))),
38
+ ),
39
+ );
40
+ }
@@ -0,0 +1,38 @@
1
+ /** Balance section: spend window, low-balance alert, and the peak/valley marker. */
2
+
3
+ import type * as React from 'react';
4
+ import { Field, Row, Seg, Toggle, groupHead, h, sectionHead } from '../ui.js';
5
+ import type { PanelApi } from '../types.js';
6
+
7
+ export function renderBalance(api: PanelApi): React.ReactElement {
8
+ const { t, settings, draft, setDraft, stage } = api;
9
+ return h('div', { className: 'drc-section' },
10
+ sectionHead(t('balance'), t('balancePageHint')),
11
+ h('div', { className: 'drc-group' },
12
+ groupHead(t('balanceSpend'), t('balanceHint')),
13
+ h('div', { className: 'drc-actions' },
14
+ h(Seg, {
15
+ options: [{ value: 'calendar', label: t('calendar') }, { value: 'rolling', label: t('rolling') }],
16
+ value: settings.balance?.spendWindow ?? 'calendar',
17
+ onChange: (value) => stage({ balance: { spendWindow: value } }),
18
+ }),
19
+ ),
20
+ ),
21
+ h('div', { className: 'drc-group' },
22
+ groupHead(t('lowThreshold'), t('lowThresholdHint')),
23
+ h('div', { className: 'drc-fields' },
24
+ h(Field, { label: t('lowThreshold'), value: draft.threshold, placeholder: t('thresholdPlaceholder'), inputMode: 'decimal', onChange: (value) => setDraft((current) => ({ ...current, threshold: value })) }),
25
+ ),
26
+ ),
27
+ h('div', { className: 'drc-group' },
28
+ groupHead(t('peakValleyBlock'), t('peakValleyHint')),
29
+ h(Row, { title: t('peakValleyToggle') },
30
+ h(Toggle, {
31
+ checked: settings.balance?.showPeakValley !== false,
32
+ label: t('peakValley'),
33
+ onChange: (checked) => stage({ balance: { showPeakValley: checked } }),
34
+ }),
35
+ ),
36
+ ),
37
+ );
38
+ }
@@ -0,0 +1,97 @@
1
+ /** Connection section: pairing, reachable interfaces, access token, and TLS. */
2
+
3
+ import type * as React from 'react';
4
+ import { Btn, Field, Row, Toggle, groupHead, h, sectionHead } from '../ui.js';
5
+ import type { PanelApi } from '../types.js';
6
+
7
+ function networkGroup(api: PanelApi): React.ReactElement {
8
+ const { t, data, toggleInterface } = api;
9
+ return h('div', { className: 'drc-group' },
10
+ groupHead(t('network'), t('networkHint')),
11
+ data.interfaces && data.interfaces.length > 0
12
+ ? h('div', { className: 'drc-list' }, data.interfaces.map((entry) => h('div', { className: 'drc-list-row', key: `${entry.name}-${entry.address}` },
13
+ h('label', null,
14
+ h('input', {
15
+ type: 'checkbox',
16
+ checked: api.selectedInterfaces.includes(entry.name),
17
+ onChange: () => toggleInterface(entry.name),
18
+ }),
19
+ h('span', null, entry.name),
20
+ ),
21
+ h('span', { className: 'drc-code' }, entry.cidr || entry.address),
22
+ )))
23
+ : h('p', { className: 'drc-empty' }, '—'),
24
+ );
25
+ }
26
+
27
+ function accessGroup(api: PanelApi): React.ReactElement {
28
+ const { t, settings, stage, busy, rotate } = api;
29
+ return h('div', { className: 'drc-group' },
30
+ groupHead(t('access'), t('accessHint')),
31
+ h(Row, { title: t('token') },
32
+ h(Toggle, { checked: settings.tokenEnabled, label: t('token'), onChange: (checked) => stage({ tokenEnabled: checked }) }),
33
+ ),
34
+ h('div', { className: 'drc-divider' }),
35
+ h('div', { className: 'drc-actions drc-actions-stack' },
36
+ h(Btn, { danger: true, disabled: busy, onClick: () => void rotate() }, t('rotate')),
37
+ h('span', { className: 'drc-row-desc' }, t('rotateHint')),
38
+ ),
39
+ );
40
+ }
41
+
42
+ function tlsGroup(api: PanelApi): React.ReactElement {
43
+ const { t, settings, draft, info, stage, setDraft, setError } = api;
44
+ return h('div', { className: 'drc-group' },
45
+ groupHead(t('tls'), t('tlsHint')),
46
+ h(Row, { title: t('tlsEnable') }, h(Toggle, {
47
+ checked: settings.tls?.enabled,
48
+ label: t('tlsEnable'),
49
+ onChange: (checked) => {
50
+ if (checked && (!draft.certPath || !draft.keyPath)) {
51
+ setError(t('tlsPathsRequired'));
52
+ return;
53
+ }
54
+ stage({ tls: { enabled: checked } });
55
+ },
56
+ })),
57
+ h('div', { className: 'drc-divider' }),
58
+ h('div', { className: 'drc-fields' },
59
+ h(Field, { label: t('certPath'), value: draft.certPath, placeholder: 'C:\\certs\\fullchain.pem', onChange: (value) => setDraft((current) => ({ ...current, certPath: value })) }),
60
+ h(Field, { label: t('keyPath'), value: draft.keyPath, placeholder: 'C:\\certs\\privkey.pem', onChange: (value) => setDraft((current) => ({ ...current, keyPath: value })) }),
61
+ ),
62
+ h('div', { className: 'drc-actions' },
63
+ h('span', { className: 'drc-row-desc' }, settings.tls?.enabled ? (info.tls?.active ? t('tlsOn') : t('tlsError')) : t('tlsOff')),
64
+ ),
65
+ );
66
+ }
67
+
68
+ function pairingGroup(api: PanelApi): React.ReactElement {
69
+ const { t, info, copied, copy } = api;
70
+ const remote = info.routes?.remote || '';
71
+ return h('div', { className: 'drc-group' },
72
+ groupHead(t('pairing'), t('pairingHint')),
73
+ h('div', { className: 'drc-qr' },
74
+ info.qrPath
75
+ ? h('div', { className: 'drc-qr-frame' }, h('img', { src: info.qrPath, alt: t('pairing'), width: 160, height: 160 }))
76
+ : h('div', { className: 'drc-qr-frame' }),
77
+ h('div', { className: 'drc-qr-copy' },
78
+ h('div', { className: 'drc-actions', style: { marginTop: 0, paddingTop: 0 } },
79
+ h(Btn, { primary: true, disabled: !remote, onClick: () => window.open(remote, '_blank', 'noopener,noreferrer') }, t('open')),
80
+ h(Btn, { disabled: !remote, onClick: () => void copy() }, copied ? t('copied') : t('copy')),
81
+ ),
82
+ h('code', { className: 'drc-url' }, remote || '—'),
83
+ info.tls?.error ? h('p', { className: 'drc-row-desc', style: { color: 'var(--drc-error)', marginTop: 8 } }, `${t('tlsError')}: ${info.tls.error}`) : null,
84
+ ),
85
+ ),
86
+ );
87
+ }
88
+
89
+ export function renderConnection(api: PanelApi): React.ReactElement {
90
+ return h('div', { className: 'drc-section' },
91
+ sectionHead(api.t('connection'), api.t('connectionHint')),
92
+ pairingGroup(api),
93
+ networkGroup(api),
94
+ accessGroup(api),
95
+ tlsGroup(api),
96
+ );
97
+ }