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

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,366 @@
1
+ /**
2
+ * Application-name mapping and history for notifications and media.
3
+ *
4
+ * The native probe reports only the raw OS application identity (a Windows
5
+ * notification handler id / SMTC AUMID, or an MPRIS bus name). This module owns
6
+ * every display name:
7
+ *
8
+ * - a built-in default table for well-known Windows apps,
9
+ * - generic derivation for executable paths, package AUMIDs and MPRIS names,
10
+ * - the history of every application the console has seen (first/last seen,
11
+ * notification count, which console page reported it),
12
+ * - user renames from the "app mapping" settings tab, persisted on disk and
13
+ * applied to both notifications and the music player.
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { isRecord } from './types.js';
19
+
20
+ /** Where an application id was observed. */
21
+ export type AppSource = 'notification' | 'music';
22
+
23
+ /** An application reported by the probe. */
24
+ export interface AppObservation {
25
+ key: string;
26
+ /** Notifications seen for this app in one snapshot (max wins). */
27
+ count?: number;
28
+ }
29
+
30
+ /** Built-in names for well-known Windows application ids. */
31
+ const DEFAULT_APP_NAMES: Record<string, string> = {
32
+ qq: 'QQ',
33
+ 'com.bilibili.bilibilipc': '哔哩哔哩',
34
+ 'bilbilipc': '哔哩哔哩',
35
+ 'com.squirrel.discord.discord': 'Discord',
36
+ 'com.oplus.devicespace': 'OPPO 互传',
37
+ 'dev.zcode.app': 'ZCode',
38
+ 'electron.app.zcode': 'ZCode',
39
+ 'firefoxtoast-308046b0af4a39cb': 'Firefox',
40
+ 'microsoft.screensketch_8wekyb3d8bbwe!app': '截图工具',
41
+ 'microsoft.windowsterminal_8wekyb3d8bbwe!app': '终端',
42
+ 'microsoft.visualstudiocode_8wekyb3d8bbwe!vscode': 'Visual Studio Code',
43
+ 'microsoft.microsoftedge.stable_8wekyb3d8bbwe!app': 'Microsoft Edge',
44
+ 'microsoft.outlookforwindows_8wekyb3d8bbwe!microsoft.outlookforwindows': 'Outlook',
45
+ 'microsoft.office.outlook.exe.15': 'Outlook',
46
+ 'microsoft.yourphone_8wekyb3d8bbwe!app': '手机连接',
47
+ 'microsoft.zunemusic_8wekyb3d8bbwe!microsoft.zunemusic': '媒体播放器',
48
+ 'microsoft.windowsstore_8wekyb3d8bbwe!app': 'Microsoft Store',
49
+ 'microsoft.todos_8wekyb3d8bbwe!app': 'Microsoft To Do',
50
+ 'microsoft.paint_8wekyb3d8bbwe!app': '画图',
51
+ 'microsoft.windowsnotepad_8wekyb3d8bbwe!app': '记事本',
52
+ 'microsoft.windowscalculator_8wekyb3d8bbwe!app': '计算器',
53
+ 'microsoft.windows.photos_8wekyb3d8bbwe!app': '照片',
54
+ 'windows.defender.securitycenter': 'Windows 安全中心',
55
+ 'microsoft.zunemusic_8wekyb3d8bbwe': '媒体播放器',
56
+ 'cloudmusic.exe': '网易云音乐',
57
+ 'qqmusic.exe': 'QQ音乐',
58
+ 'kugou.exe': '酷狗音乐',
59
+ 'kgmusic.exe': '酷狗音乐',
60
+ 'kwmusic.exe': '酷我音乐',
61
+ 'kwservice.exe': '酷我音乐',
62
+ 'sodamusic.exe': '汽水音乐',
63
+ 'wesing.exe': '全民K歌',
64
+ 'spotify.exe': 'Spotify',
65
+ 'applemusic.exe': 'Apple Music',
66
+ 'youtube music.exe': 'YouTube Music',
67
+ 'cider.exe': 'Cider',
68
+ };
69
+
70
+ /** One persisted application entry, as stored in `app-mapping.json`. */
71
+ interface StoredApp {
72
+ key: string;
73
+ customName: string | null;
74
+ firstSeen: number;
75
+ lastSeen: number;
76
+ count: number;
77
+ sources: string[];
78
+ }
79
+
80
+ /** Browser-facing application entry. */
81
+ export interface MappedApp {
82
+ key: string;
83
+ name: string;
84
+ /** User override, or `null` when the default name is used. */
85
+ customName: string | null;
86
+ /** Built-in/derived name, `null` when nothing better than the raw id exists. */
87
+ defaultName: string | null;
88
+ count: number;
89
+ firstSeen: number;
90
+ lastSeen: number;
91
+ sources: string[];
92
+ }
93
+
94
+ /** Storage API returned by {@link createAppMapping}. */
95
+ export interface AppMapping {
96
+ record(observations: AppObservation[], source: AppSource): void;
97
+ rename(key: string, name: string | null): MappedApp | null;
98
+ displayName(key: string): string;
99
+ list(): MappedApp[];
100
+ get(key: string): MappedApp | null;
101
+ flush(): void;
102
+ dispose(): void;
103
+ }
104
+
105
+ const MAX_APPS = 300;
106
+ const MAX_NAME_CHARS = 60;
107
+ const MAX_KEY_CHARS = 240;
108
+
109
+ /** Turn `SomePascalCaseName` into `Some Pascal Case Name`. */
110
+ function spacesBetweenWords(value: string): string {
111
+ let output = '';
112
+ let previousLower = false;
113
+ for (const character of value) {
114
+ if (character === '-' || character === '_') {
115
+ output += ' ';
116
+ } else {
117
+ if (character === character.toUpperCase() && character !== character.toLowerCase() && previousLower) {
118
+ output += ' ';
119
+ }
120
+ output += character;
121
+ }
122
+ previousLower = character === character.toLowerCase() && character !== character.toUpperCase();
123
+ }
124
+ return output.trim();
125
+ }
126
+
127
+ /** Derive a readable name from a raw Windows/Linux application id. */
128
+ export function defaultAppName(rawKey: string): string | null {
129
+ const key = String(rawKey || '').trim();
130
+ if (!key) return null;
131
+ const exact = DEFAULT_APP_NAMES[key.toLowerCase()];
132
+ if (exact) return exact;
133
+
134
+ // Windows executable path (desktop notification handler).
135
+ if ((key.includes('\\') || key.includes('/')) && key.toLowerCase().endsWith('.exe')) {
136
+ const base = key.split(/[\\/]/).pop() || key;
137
+ return base.replace(/\.exe$/i, '') || null;
138
+ }
139
+ if (key.startsWith('NotifyIconGeneratedAumid_')) return '桌面应用';
140
+ if (key.toLowerCase().startsWith('microsoft.microsoftedge')) return 'Microsoft Edge';
141
+ if (key.toLowerCase().startsWith('microsoft.screensketch')) return '截图工具';
142
+
143
+ // Generic media-source heuristics (SMTC AUMIDs and browser processes).
144
+ if (/bilibili/i.test(key)) return '哔哩哔哩';
145
+ if (/youtube[-_ ]?music|ytmusic/i.test(key)) return 'YouTube Music';
146
+ if (/cloudmusic|netease|163music|网易云/i.test(key)) return '网易云音乐';
147
+ if (/qqmusic|qq音乐/i.test(key)) return 'QQ音乐';
148
+ if (/kugou|kgmusic|酷狗/i.test(key)) return '酷狗音乐';
149
+ if (/kwmusic|kwservice|kuwo|酷我/i.test(key)) return '酷我音乐';
150
+ if (/sodamusic|汽水音乐/i.test(key)) return '汽水音乐';
151
+ if (/wesing|全民k歌/i.test(key)) return '全民K歌';
152
+ if (/cider/i.test(key)) return 'Cider';
153
+ if (/applemusic|apple[ .]music|com\.apple\.music/i.test(key)) return 'Apple Music';
154
+ if (/spotify/i.test(key)) return 'Spotify';
155
+ if (/msedge/i.test(key)) return 'Microsoft Edge';
156
+ if (/brave/i.test(key)) return 'Brave';
157
+ if (/vivaldi/i.test(key)) return 'Vivaldi';
158
+ if (/firefox/i.test(key)) return 'Firefox';
159
+ if (/chrome/i.test(key)) return 'Chrome';
160
+
161
+ // MPRIS bus name: org.mpris.MediaPlayer2.<player>
162
+ if (key.startsWith('org.mpris.MediaPlayer2.')) {
163
+ const tail = key.slice('org.mpris.MediaPlayer2.'.length).replace(/[._-]+/g, ' ').trim();
164
+ return tail ? tail.replace(/\b\w/g, (letter) => letter.toUpperCase()) : null;
165
+ }
166
+
167
+ // Windows package AUMID: Package_family!EntryPoint
168
+ let base = key.split('!')[0];
169
+ const underscore = base.lastIndexOf('_');
170
+ if (underscore > 0) {
171
+ const suffix = base.slice(underscore + 1);
172
+ if (suffix.length >= 8 && /^[a-z0-9]+$/.test(suffix)) base = base.slice(0, underscore);
173
+ }
174
+ const tail = base.split('.').filter(Boolean).pop() || '';
175
+ const spaced = spacesBetweenWords(tail);
176
+ return spaced || null;
177
+ }
178
+
179
+ function sanitizeName(value: unknown): string | null {
180
+ if (typeof value !== 'string') return null;
181
+ const name = value.replace(/\s+/g, ' ').trim().slice(0, MAX_NAME_CHARS);
182
+ return name || null;
183
+ }
184
+
185
+ function sanitizeSources(value: unknown): string[] {
186
+ if (!Array.isArray(value)) return [];
187
+ return [...new Set(value.filter((item): item is AppSource => item === 'notification' || item === 'music'))];
188
+ }
189
+
190
+ /** Rebuild the persisted entry map, dropping records that make no sense. */
191
+ function readEntries(value: unknown): Record<string, StoredApp> {
192
+ const apps: Record<string, StoredApp> = {};
193
+ if (!isRecord(value)) return apps;
194
+ for (const entry of Object.values(value)) {
195
+ if (!isRecord(entry) || typeof entry.key !== 'string' || !entry.key) continue;
196
+ const key = entry.key.slice(0, MAX_KEY_CHARS);
197
+ apps[key] = {
198
+ key,
199
+ customName: sanitizeName(entry.customName),
200
+ firstSeen: Number(entry.firstSeen) || Number(entry.lastSeen) || 0,
201
+ lastSeen: Number(entry.lastSeen) || 0,
202
+ count: Math.max(0, Math.round(Number(entry.count) || 0)),
203
+ sources: sanitizeSources(entry.sources),
204
+ };
205
+ }
206
+ return apps;
207
+ }
208
+
209
+ /** Seed the history from the pre-mapping `notification-cache.json`, if any. */
210
+ function readLegacyCache(directory: string): Record<string, StoredApp> {
211
+ const apps: Record<string, StoredApp> = {};
212
+ try {
213
+ const parsed: unknown = JSON.parse(fs.readFileSync(path.join(directory, 'notification-cache.json'), 'utf8'));
214
+ if (!isRecord(parsed) || !isRecord(parsed.apps)) return apps;
215
+ for (const entry of Object.values(parsed.apps)) {
216
+ if (!isRecord(entry) || typeof entry.key !== 'string' || !entry.key) continue;
217
+ const key = entry.key.slice(0, MAX_KEY_CHARS);
218
+ apps[key] = {
219
+ key,
220
+ customName: null,
221
+ firstSeen: Number(entry.lastSeen) || 0,
222
+ lastSeen: Number(entry.lastSeen) || 0,
223
+ count: Math.max(0, Math.round(Number(entry.count) || 0)),
224
+ sources: ['notification'],
225
+ };
226
+ }
227
+ } catch {
228
+ /* no legacy cache */
229
+ }
230
+ return apps;
231
+ }
232
+
233
+ export function createAppMapping(directory: string): AppMapping {
234
+ const dir = directory;
235
+ const file = path.join(dir, 'app-mapping.json');
236
+ fs.mkdirSync(dir, { recursive: true });
237
+
238
+ let apps: Record<string, StoredApp> = {};
239
+ let loaded = false;
240
+ try {
241
+ const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
242
+ if (isRecord(parsed) && isRecord(parsed.apps)) {
243
+ apps = readEntries(parsed.apps);
244
+ loaded = true;
245
+ }
246
+ } catch {
247
+ apps = {};
248
+ }
249
+ if (!loaded) apps = readLegacyCache(dir);
250
+
251
+ let timer: NodeJS.Timeout | null = null;
252
+
253
+ function resolve(entry: StoredApp): MappedApp {
254
+ const defaultName = defaultAppName(entry.key);
255
+ return {
256
+ key: entry.key,
257
+ name: entry.customName || defaultName || entry.key,
258
+ customName: entry.customName,
259
+ defaultName,
260
+ count: entry.count,
261
+ firstSeen: entry.firstSeen,
262
+ lastSeen: entry.lastSeen,
263
+ sources: [...entry.sources],
264
+ };
265
+ }
266
+
267
+ function flush(): void {
268
+ timer = null;
269
+ const temporary = `${file}.tmp-${process.pid}`;
270
+ try {
271
+ fs.writeFileSync(temporary, JSON.stringify({ version: 1, updatedAt: Date.now(), apps }, null, 2), 'utf8');
272
+ fs.renameSync(temporary, file);
273
+ } catch {
274
+ /* mapping must never break notifications */
275
+ }
276
+ }
277
+
278
+ function schedule(): void {
279
+ if (timer) return;
280
+ timer = setTimeout(flush, 1200);
281
+ timer.unref?.();
282
+ }
283
+
284
+ function trimToLimit(): void {
285
+ const entries = Object.values(apps);
286
+ if (entries.length <= MAX_APPS) return;
287
+ entries.sort((left, right) => right.lastSeen - left.lastSeen);
288
+ apps = Object.fromEntries(entries.slice(0, MAX_APPS).map((entry) => [entry.key, entry]));
289
+ }
290
+
291
+ function record(observations: AppObservation[], source: AppSource): void {
292
+ if (!Array.isArray(observations) || observations.length === 0) return;
293
+ let changed = false;
294
+ const now = Date.now();
295
+ for (const observation of observations) {
296
+ if (!isRecord(observation)) continue;
297
+ const rawKey = typeof observation.key === 'string' ? observation.key.trim() : '';
298
+ if (!rawKey) continue;
299
+ const key = rawKey.slice(0, MAX_KEY_CHARS);
300
+ const count = Math.max(0, Math.round(Number(observation.count) || 0));
301
+ const previous = apps[key];
302
+ const sources = new Set(previous?.sources ?? []);
303
+ sources.add(source);
304
+ const sourcesChanged = sources.size !== (previous?.sources.length ?? 0);
305
+ // Only refresh the timestamp when something changed or the last sighting
306
+ // is old: the console polls notifications every few seconds and the file
307
+ // should not be rewritten on every poll.
308
+ const stale = !previous || now - previous.lastSeen > 60_000;
309
+ if (previous && previous.count === count && !sourcesChanged && !stale) continue;
310
+ apps[key] = {
311
+ key,
312
+ customName: previous?.customName ?? null,
313
+ firstSeen: previous?.firstSeen || now,
314
+ lastSeen: now,
315
+ count: Math.max(count, previous?.count ?? 0),
316
+ sources: [...sources],
317
+ };
318
+ changed = true;
319
+ }
320
+ if (changed) {
321
+ trimToLimit();
322
+ schedule();
323
+ }
324
+ }
325
+
326
+ function rename(key: string, name: string | null): MappedApp | null {
327
+ const rawKey = String(key || '').trim();
328
+ if (!rawKey) return null;
329
+ const trimmedKey = rawKey.slice(0, MAX_KEY_CHARS);
330
+ const previous = apps[trimmedKey];
331
+ if (!previous) return null;
332
+ const customName = sanitizeName(name);
333
+ if (previous.customName === customName) return resolve(previous);
334
+ apps[trimmedKey] = { ...previous, customName };
335
+ flush();
336
+ return resolve(apps[trimmedKey]);
337
+ }
338
+
339
+ function displayName(key: string): string {
340
+ const trimmedKey = String(key || '').trim().slice(0, MAX_KEY_CHARS);
341
+ if (!trimmedKey) return '未知应用';
342
+ const stored = apps[trimmedKey];
343
+ if (stored?.customName) return stored.customName;
344
+ return defaultAppName(trimmedKey) || trimmedKey;
345
+ }
346
+
347
+ function list(): MappedApp[] {
348
+ return Object.values(apps)
349
+ .map(resolve)
350
+ .sort((left, right) => right.lastSeen - left.lastSeen || right.count - left.count);
351
+ }
352
+
353
+ function get(key: string): MappedApp | null {
354
+ const stored = apps[String(key || '').trim().slice(0, MAX_KEY_CHARS)];
355
+ return stored ? resolve(stored) : null;
356
+ }
357
+
358
+ function dispose(): void {
359
+ if (timer) {
360
+ clearTimeout(timer);
361
+ flush();
362
+ }
363
+ }
364
+
365
+ return { record, rename, displayName, list, get, flush, dispose };
366
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Host-side copy for strings the plugin itself sends to the phone.
3
+ *
4
+ * The browser surfaces have their own dictionaries (`src/dsh/client/i18n.ts`
5
+ * for the DSH panel and `src/svelte/lib/i18n.svelte.ts` for the console). The
6
+ * Host cannot import either: one is a browser module factory and the other is
7
+ * a Vite bundle, while this file is compiled into `lib/`. It therefore keeps
8
+ * the same layout — one flat key -> text object per language plus a DICTS
9
+ * table — so all three dictionaries stay readable and a new language can be
10
+ * added the same way in each.
11
+ *
12
+ * These keys cover only text the Host generates and ships with a payload
13
+ * (session fallbacks, answer validation, the pre-app pairing page).
14
+ */
15
+
16
+ import type { PluginLocale } from './settings.js';
17
+
18
+ export const zh = {
19
+ answerWaiting: '正在等待你的回答',
20
+ answerCountMismatch: '回答数量不匹配:需要 {expected} 项,收到 {received} 项',
21
+ answerDuplicate: '回答项重复或缺少问题编号',
22
+ answerUnknownQuestion: '回答项不属于当前问题',
23
+ answerSingleChoice: '单选题只能选择一个选项',
24
+ answerOptionGone: '选择的选项已不在问题中',
25
+ answerMissingQuestion: '回答缺少问题',
26
+ answerExpired: '这个问题已经结束或不在等待回答',
27
+ answerNeedsDsh: '这个问题需要在 DSH 中回答',
28
+ answerMissingId: '缺少问题编号,无法回答',
29
+ answerUnsupported: '当前 DSH 不支持远程回答',
30
+ answerNotWaiting: '这个问题已经不再等待回答',
31
+ answerPushOff: '推送问询已关闭',
32
+ answerInvalid: '回答格式无效',
33
+ approvalPrompt: '需要允许 {tool} 执行',
34
+ approvalTool: '工具',
35
+ newSession: '新会话',
36
+ turnFailed: '本轮运行出错',
37
+ turnBlocked: '本轮被阻止',
38
+ pairingTitle: '需要配对',
39
+ pairingBody: '请从 DSH 侧边栏的远程控制台重新扫码。',
40
+ } as const;
41
+
42
+ export type HostMessageKey = keyof typeof zh;
43
+
44
+ export const en: Record<HostMessageKey, string> = {
45
+ answerWaiting: 'Waiting for your answer',
46
+ answerCountMismatch: 'Answer count mismatch: expected {expected}, received {received}',
47
+ answerDuplicate: 'Duplicate answer or missing question id',
48
+ answerUnknownQuestion: 'An answer does not belong to the current question',
49
+ answerSingleChoice: 'A single-choice question accepts one option',
50
+ answerOptionGone: 'A selected option is no longer part of the question',
51
+ answerMissingQuestion: 'An answer is missing for one question',
52
+ answerExpired: 'This question has finished or is no longer waiting',
53
+ answerNeedsDsh: 'Answer this question in DSH',
54
+ answerMissingId: 'The question id is missing; cannot answer',
55
+ answerUnsupported: 'This DSH build does not support remote answers',
56
+ answerNotWaiting: 'This question is no longer waiting for an answer',
57
+ answerPushOff: 'Pushed inquiries are turned off',
58
+ answerInvalid: 'The answer format is invalid',
59
+ approvalPrompt: 'Allow {tool} to run',
60
+ approvalTool: 'the tool',
61
+ newSession: 'New session',
62
+ turnFailed: 'This turn failed',
63
+ turnBlocked: 'This turn was blocked',
64
+ pairingTitle: 'Pairing required',
65
+ pairingBody: 'Scan the QR code again from the Remote control panel in the DSH sidebar.',
66
+ };
67
+
68
+ /** Every shipped dictionary, keyed by locale code. */
69
+ export const DICTS: Record<PluginLocale, Record<HostMessageKey, string>> = { zh, en };
70
+
71
+ /** Translate one Host message; unknown placeholders stay verbatim. */
72
+ export function hostMessage(
73
+ locale: PluginLocale,
74
+ key: HostMessageKey,
75
+ params?: Record<string, string | number>,
76
+ ): string {
77
+ const template = DICTS[locale]?.[key] ?? en[key];
78
+ if (!params) return template;
79
+ return template.replace(/\{(\w+)\}/g, (match, name: string) =>
80
+ name in params ? String(params[name]) : match,
81
+ );
82
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Shared media enrichment for the Rust probe result.
3
+ *
4
+ * It normalises the raw payload, projects the timeline between polls, and
5
+ * decides whether seeking is trustworthy. NetEase cover/liked data is resolved
6
+ * in Rust (`vendor::netease`) and arrives already attached to the payload.
7
+ */
8
+
9
+ import type { ProbeMediaResult, PublicMedia } from './types.js';
10
+ import { clamp, finiteNumber } from './util.js';
11
+
12
+ /** Smoothed playback position for one track, carried between polls. */
13
+ interface ProgressState {
14
+ key: string;
15
+ position: number;
16
+ at: number;
17
+ playing: boolean;
18
+ }
19
+
20
+ /** A seek this adapter just issued; the player needs a moment to catch up. */
21
+ interface PendingSeek {
22
+ key: string;
23
+ position: number;
24
+ until: number;
25
+ }
26
+
27
+ /** Whether the current track has ever reported a usable position. */
28
+ interface PositionEvidence {
29
+ key: string;
30
+ seen: boolean;
31
+ playing: boolean;
32
+ }
33
+
34
+ /** Stateful enricher shared by one media adapter. */
35
+ export interface MediaEnrichment {
36
+ enrich(result: ProbeMediaResult | null | undefined): Promise<PublicMedia | null>;
37
+ markSeek(key: string, position: number, until?: number): void;
38
+ reset(): void;
39
+ }
40
+
41
+ export function publicMedia(result: ProbeMediaResult | null | undefined): PublicMedia | null {
42
+ const media = result?.media;
43
+ if (!result || !media) return null;
44
+ const provider: PublicMedia['provider'] =
45
+ media.provider === 'private' || media.provider === 'accessibility' ? media.provider : 'system';
46
+ return {
47
+ appId: media.appId || '',
48
+ appName: '',
49
+ title: media.title || '未命名媒体',
50
+ artist: media.artist || '',
51
+ album: media.album || '',
52
+ albumArtist: media.albumArtist || '',
53
+ playbackStatus: media.playbackStatus || 'Closed',
54
+ autoRepeatMode: media.autoRepeatMode || '',
55
+ shuffleActive: Boolean(media.shuffleActive),
56
+ position: Math.max(0, finiteNumber(media.position, 0) || 0),
57
+ startTime: Math.max(0, finiteNumber(media.startTime, 0) || 0),
58
+ endTime: Math.max(0, finiteNumber(media.endTime, 0) || 0),
59
+ minSeekTime: Math.max(0, finiteNumber(media.minSeekTime, 0) || 0),
60
+ maxSeekTime: Math.max(0, finiteNumber(media.maxSeekTime, 0) || 0),
61
+ lastUpdatedAt: finiteNumber(media.lastUpdatedAt, null),
62
+ canPlayPause: media.canPlayPause !== false,
63
+ canNext: media.canNext !== false,
64
+ canPrevious: media.canPrevious !== false,
65
+ canSeek: Boolean(media.canSeek),
66
+ provider,
67
+ providerDetail: media.providerDetail ? String(media.providerDetail) : null,
68
+ thumbnail: media.thumbnail || null,
69
+ thumbnailUrl: media.thumbnailUrl || null,
70
+ thumbnailType: media.thumbnailType || null,
71
+ liked: typeof media.liked === 'boolean' ? media.liked : null,
72
+ canLike: Boolean(media.canLike),
73
+ canSetLiked: Boolean(media.canSetLiked),
74
+ trackId: media.trackId || null,
75
+ estimatedDuration: Boolean(media.estimatedDuration),
76
+ sampledAt: finiteNumber(result.sampledAt, Date.now()) || Date.now(),
77
+ };
78
+ }
79
+
80
+ export function mediaKey(media: PublicMedia | null | undefined): string {
81
+ return `${media?.appId || ''}|${media?.title || ''}|${media?.artist || ''}`;
82
+ }
83
+
84
+ function timelineAnchor(value: number | null, sampledAt: number): number | null {
85
+ const anchor = finiteNumber(value, null);
86
+ if (!anchor || anchor <= 0) return null;
87
+ const age = sampledAt - anchor;
88
+ return age >= -5000 && age <= 6 * 60 * 60 * 1000 ? anchor : null;
89
+ }
90
+
91
+ /**
92
+ * Creates a stateful enricher. One instance per media adapter (the smoothing and
93
+ * "has this player ever reported a position" evidence are per adapter).
94
+ */
95
+ export function createMediaEnrichment(): MediaEnrichment {
96
+ let progress: ProgressState | null = null;
97
+ let pendingSeek: PendingSeek | null = null;
98
+ let positionEvidence: PositionEvidence | null = null;
99
+
100
+ async function enrich(result: ProbeMediaResult | null | undefined): Promise<PublicMedia | null> {
101
+ const sampledAt = finiteNumber(result?.sampledAt, Date.now()) || Date.now();
102
+ let media = publicMedia(result);
103
+ if (!media) {
104
+ progress = null;
105
+ pendingSeek = null;
106
+ positionEvidence = null;
107
+ return null;
108
+ }
109
+
110
+ const rawStart = Math.max(0, Number(media.startTime) || 0);
111
+ const rawEnd = Math.max(0, Number(media.endTime) || 0);
112
+ const rawPosition = Math.max(0, Number(media.position) || 0);
113
+ const hasTimeline = rawEnd > rawStart && rawEnd > 0;
114
+ const systemProvider = media.provider === 'system';
115
+ const playing = String(media.playbackStatus).toLowerCase() === 'playing';
116
+ const key = mediaKey(media);
117
+
118
+ // A seekable flag can be true even when a player never reports a usable
119
+ // position (some older clients do this). Only allow seeking after we have
120
+ // actually seen a non-zero position for this track.
121
+ if (!positionEvidence || positionEvidence.key !== key) {
122
+ positionEvidence = { key, seen: rawPosition > 0.01, playing };
123
+ } else {
124
+ // Replaying the same track from 0 starts a new position session even if
125
+ // the title/artist key is unchanged; do not inherit the old seek grant.
126
+ if (playing && !positionEvidence.playing && rawPosition <= 0.01) positionEvidence.seen = false;
127
+ if (rawPosition > 0.01) positionEvidence.seen = true;
128
+ positionEvidence.playing = playing;
129
+ }
130
+ const hasPositionReport = positionEvidence.seen;
131
+ // Only a complete system session (SMTC/MPRIS/MediaRemote) may seek. The
132
+ // NetEase CDP page supplies progress and transport control, but never a
133
+ // seekable timeline unless the system session explicitly reports one.
134
+ const canSeek =
135
+ Boolean(media.canSeek) && systemProvider && hasTimeline && hasPositionReport;
136
+ const timelineSource: PublicMedia['timelineSource'] = !hasTimeline
137
+ ? 'none'
138
+ : systemProvider
139
+ ? 'smtc'
140
+ : media.provider === 'accessibility'
141
+ ? 'accessibility'
142
+ : 'private';
143
+
144
+ media.canSeek = canSeek;
145
+ media.positionReported = hasPositionReport;
146
+ media.timelineSource = timelineSource;
147
+ media.estimatedDuration = false;
148
+
149
+ const end = Math.max(0, Number(media.endTime) || 0);
150
+ const start = Math.max(0, Number(media.startTime) || 0);
151
+ const duration = end > start ? end - start : end;
152
+ const clampPosition = (value: number): number =>
153
+ (duration > 0 ? clamp(value, start, start + duration) : Math.max(0, value));
154
+
155
+ if (pendingSeek && (pendingSeek.key !== key || sampledAt >= pendingSeek.until)) pendingSeek = null;
156
+
157
+ // SMTC timelines report Position at LastUpdatedTime. Project it forward for
158
+ // players that only refresh the timeline a few times per track.
159
+ const anchor = timelineAnchor(media.lastUpdatedAt, sampledAt);
160
+ let reported = rawPosition;
161
+ if (playing && hasTimeline && anchor) reported += (sampledAt - anchor) / 1000;
162
+ reported = clampPosition(reported);
163
+
164
+ if (!progress || progress.key !== key) {
165
+ progress = { key, position: reported, at: sampledAt, playing };
166
+ } else {
167
+ const elapsed = progress.playing ? Math.max(0, (sampledAt - progress.at) / 1000) : 0;
168
+ let position = clampPosition(progress.position + elapsed);
169
+ // Treat 0 as "no position report" while playing: several players expose a
170
+ // duration but keep Position at 0, and snapping back would freeze the bar.
171
+ const reportUsable = rawPosition > 0.01;
172
+ if (reportUsable) {
173
+ const delta = reported - position;
174
+ if (Math.abs(delta) > 2.5) position = reported;
175
+ else if (Math.abs(delta) > 0.35) position = clampPosition(position + delta * 0.45);
176
+ }
177
+ if (playing !== progress.playing && reportUsable) position = reported;
178
+ progress = { key, position, at: sampledAt, playing };
179
+ }
180
+
181
+ if (pendingSeek) {
182
+ progress = { key, position: clampPosition(pendingSeek.position), at: sampledAt, playing };
183
+ }
184
+
185
+ media.position = progress.position;
186
+ media.sampledAt = sampledAt;
187
+ return media;
188
+ }
189
+
190
+ /** Hold a requested seek position while the player catches up. */
191
+ function markSeek(key: string, position: number, until: number = Date.now() + 2600): void {
192
+ pendingSeek = { key, position, until };
193
+ }
194
+
195
+ function reset(): void {
196
+ progress = null;
197
+ pendingSeek = null;
198
+ positionEvidence = null;
199
+ }
200
+
201
+ return { enrich, markSeek, reset };
202
+ }