@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,574 @@
1
+ #!/usr/bin/env python3
2
+ """Serve the built web console with deterministic demo data for screenshots.
3
+
4
+ This is development tooling, not part of the plugin runtime. It copies the
5
+ Vite output to a temporary directory, injects a fake WebSocket that emits the
6
+ same envelopes as the Host, and serves the result with a short artificial load
7
+ delay so a screen capture includes the rendered data.
8
+
9
+ Usage (run after `pnpm build:web`):
10
+
11
+ python3 scripts/mock-preview.py --web web --port 4173 --locale zh --theme dark --freeze-clock
12
+ # http://127.0.0.1:4173/?page=music
13
+ # http://127.0.0.1:4173/?page=home&sheet=1 (opens the question dialog)
14
+
15
+ For README images, show the preview on the real device screen
16
+ (`scripts/screenshot-device.sh`) and compose the dark/light captures with
17
+ `scripts/screenshot-compose.py`. `--freeze-clock` keeps the rendered clock
18
+ identical across the two captures.
19
+
20
+ The mock cover is a generated SVG; no image library or network access needed.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import functools
27
+ import http.server
28
+ import json
29
+ import math
30
+ import shutil
31
+ import socketserver
32
+ import sys
33
+ import time
34
+ from datetime import datetime
35
+ from pathlib import Path
36
+
37
+
38
+ ROOT = Path(__file__).resolve().parent.parent
39
+
40
+
41
+ def iso_at(hour: int, minute: int = 0) -> str:
42
+ return f"{datetime.now():%Y-%m-%d}T{hour:02d}:{minute:02d}:00"
43
+
44
+
45
+ def ago(seconds: int) -> int:
46
+ return int(time.time() * 1000) - seconds * 1000
47
+
48
+
49
+ # Demo copy that exists only in the mock payloads. Keep it exact: the English
50
+ # preview translates whole strings, so changing a sentence here also changes the
51
+ # English preview.
52
+ DEMO_EN = {
53
+ "演示城区": "Demo City",
54
+ "日历": "Calendar",
55
+ "产品评审会": "Product review",
56
+ "今天 18:30,线上会议 · 预计 45 分钟": "Today 18:30, online meeting · ~45 min",
57
+ "邮件": "Mail",
58
+ "新的设计交接说明": "New design handoff notes",
59
+ "附带了最新的界面规范和组件清单,请查收。": "Includes the latest UI spec and component list.",
60
+ "消息中心": "Messages",
61
+ "存储空间提醒": "Storage reminder",
62
+ "本月的备份已完成,当前占用空间 62%。": "This month's backup is complete; 62% used.",
63
+ "提醒事项": "Reminders",
64
+ "整理明天的待办": "Tidy tomorrow's to-do",
65
+ "还有 3 项任务没有完成": "3 tasks are still open",
66
+ "系统": "System",
67
+ "更新已就绪": "Update ready",
68
+ "新的系统更新将在空闲时安装。": "The new system update will install when idle.",
69
+ "本周": "This week",
70
+ "六": "Sat",
71
+ "日": "Sun",
72
+ "一": "Mon",
73
+ "二": "Tue",
74
+ "三": "Wed",
75
+ "四": "Thu",
76
+ "五": "Fri",
77
+ "重写远端控制台界面": "Rewrite the remote console UI",
78
+ "正在核对页面间距与卡片层级…": "Checking page spacing and card hierarchy…",
79
+ "拆分组件样式": "Split component styles",
80
+ "把全局规则收回各自的 Svelte 组件。": "Move global rules back into their Svelte components.",
81
+ "核对配色与间距": "Check colors and spacing",
82
+ "颜色变量与卡片间距已统一。": "Color variables and card spacing are now unified.",
83
+ "确认首页信息密度": "Confirm home information density",
84
+ "时钟、天气与通知的层级是否合适?": "Are the clock, weather and notification levels right?",
85
+ "首页": "Home",
86
+ "首页右侧的通知区域是否保留完整列表?": "Should the home screen keep the full notification list on the right?",
87
+ "保留完整列表(推荐)": "Keep the full list (Recommended)",
88
+ "可以滚动查看全部通知,信息更完整。": "Scroll to see every notification; more complete.",
89
+ "只显示前三条": "Show only the first three",
90
+ "视觉更轻,但会隐藏部分通知。": "Lighter visually, but hides some notifications.",
91
+ "移到独立页面": "Move to a separate page",
92
+ "首页更专注,但需要多一次跳转。": "A more focused home, at the cost of one extra tap.",
93
+ "整理演示数据": "Tidy demo data",
94
+ "已生成用于展示的示例内容。": "Generated sample content for the demo.",
95
+ "磁盘 0": "Disk 0",
96
+ "磁盘 1": "Disk 1",
97
+ }
98
+
99
+
100
+ def translate_demo(value, locale: str):
101
+ if locale != "en":
102
+ return value
103
+ if isinstance(value, str):
104
+ return DEMO_EN.get(value, value)
105
+ if isinstance(value, list):
106
+ return [translate_demo(item, locale) for item in value]
107
+ if isinstance(value, dict):
108
+ return {key: translate_demo(item, locale) for key, item in value.items()}
109
+ return value
110
+
111
+
112
+
113
+ def demo_payloads(locale: str = "zh", timer: str = "pomodoro", theme: str = "dark") -> list[dict]:
114
+ settings = {
115
+ "theme": theme,
116
+ "locale": locale,
117
+ "temperatureUnit": "celsius",
118
+ "pushQuestions": True,
119
+ "navPosition": "left",
120
+ "pageOrder": ["home", "sessions", "music", "performance"],
121
+ "pageEnabled": {"home": True, "sessions": True, "music": True, "performance": True},
122
+ "pomodoro": {
123
+ "enabled": timer == "pomodoro",
124
+ "focusMinutes": 25,
125
+ "breakMinutes": 5,
126
+ "soundPath": "",
127
+ "loopAlarm": False,
128
+ },
129
+ "countdown": {
130
+ "enabled": timer == "countdown",
131
+ "presets": [60, 300, 600],
132
+ },
133
+ "balance": {"spendWindow": "week", "lowThreshold": 50, "showPeakValley": True},
134
+ }
135
+
136
+ weather = {
137
+ "ok": True,
138
+ "location": {"name": "演示城区"},
139
+ "current": {
140
+ "code": 2,
141
+ "temperature": 19.4,
142
+ "feelsLike": 18.2,
143
+ "humidity": 58,
144
+ "windSpeed": 12,
145
+ "isDay": True,
146
+ },
147
+ "daily": {
148
+ "high": 23.8,
149
+ "low": 16.9,
150
+ "sunrise": iso_at(6, 22),
151
+ "sunset": iso_at(17, 47),
152
+ "tomorrowHigh": 24,
153
+ "tomorrowLow": 17,
154
+ },
155
+ "hourly": [
156
+ {"time": iso_at(17), "code": 2, "temperature": 19},
157
+ {"time": iso_at(18), "code": 1, "temperature": 18},
158
+ {"time": iso_at(19), "code": 1, "temperature": 17},
159
+ {"time": iso_at(20), "code": 3, "temperature": 17},
160
+ {"time": iso_at(21), "code": 3, "temperature": 16},
161
+ {"time": iso_at(22), "code": 61, "temperature": 16},
162
+ ],
163
+ "dailyForecast": [],
164
+ "forecastMode": "hourly",
165
+ }
166
+
167
+ notifications = {
168
+ "available": True,
169
+ "notifications": [
170
+ {
171
+ "id": "n1",
172
+ "appKey": "calendar",
173
+ "appName": "日历",
174
+ "title": "产品评审会",
175
+ "body": "今天 18:30,线上会议 · 预计 45 分钟",
176
+ "receivedAt": ago(8 * 60),
177
+ },
178
+ {
179
+ "id": "n2",
180
+ "appKey": "mail",
181
+ "appName": "邮件",
182
+ "title": "新的设计交接说明",
183
+ "body": "附带了最新的界面规范和组件清单,请查收。",
184
+ "receivedAt": ago(27 * 60),
185
+ },
186
+ {
187
+ "id": "n3",
188
+ "appKey": "messages",
189
+ "appName": "消息中心",
190
+ "title": "存储空间提醒",
191
+ "body": "本月的备份已完成,当前占用空间 62%。",
192
+ "receivedAt": ago(45 * 60),
193
+ },
194
+ {
195
+ "id": "n4",
196
+ "appKey": "todo",
197
+ "appName": "提醒事项",
198
+ "title": "整理明天的待办",
199
+ "body": "还有 3 项任务没有完成",
200
+ "receivedAt": ago(60 * 60),
201
+ },
202
+ {
203
+ "id": "n5",
204
+ "appKey": "system",
205
+ "appName": "系统",
206
+ "title": "更新已就绪",
207
+ "body": "新的系统更新将在空闲时安装。",
208
+ "receivedAt": ago(2 * 60 * 60),
209
+ },
210
+ ],
211
+ }
212
+
213
+ account = {
214
+ "available": True,
215
+ "signedIn": True,
216
+ "lowBalance": True,
217
+ "lowThreshold": 50,
218
+ "balance": {
219
+ "currency": "CNY",
220
+ "balance": 12.34,
221
+ "paid": 12.34,
222
+ "bonus": 0,
223
+ "state": "ready",
224
+ },
225
+ "usage": {
226
+ "currency": "CNY",
227
+ "state": "ready",
228
+ "today": 9.05,
229
+ "spend": 78.82,
230
+ "spendLabel": "本周",
231
+ "daily": [
232
+ {"label": "六", "cost": 6.4},
233
+ {"label": "日", "cost": 4.2},
234
+ {"label": "一", "cost": 18.6},
235
+ {"label": "二", "cost": 12.8},
236
+ {"label": "三", "cost": 15.4},
237
+ {"label": "四", "cost": 12.3},
238
+ {"label": "五", "cost": 9.05},
239
+ ],
240
+ },
241
+ }
242
+
243
+ sessions = [
244
+ {
245
+ "id": "s-main",
246
+ "title": "重写远端控制台界面",
247
+ "status": "running",
248
+ "preview": "正在核对页面间距与卡片层级…",
249
+ "turn": 4,
250
+ "runningSince": ago(3 * 60 + 43),
251
+ "lastActiveAt": ago(12),
252
+ },
253
+ {
254
+ "id": "s-child-1",
255
+ "title": "拆分组件样式",
256
+ "status": "running",
257
+ "preview": "把全局规则收回各自的 Svelte 组件。",
258
+ "turn": 2,
259
+ "runningSince": ago(79),
260
+ "lastActiveAt": ago(8),
261
+ "parentId": "s-main",
262
+ "isSubagent": True,
263
+ },
264
+ {
265
+ "id": "s-child-2",
266
+ "title": "核对配色与间距",
267
+ "status": "done",
268
+ "preview": "颜色变量与卡片间距已统一。",
269
+ "turn": 5,
270
+ "lastActiveAt": ago(6 * 60),
271
+ "parentId": "s-main",
272
+ "isSubagent": True,
273
+ },
274
+ {
275
+ "id": "s-question",
276
+ "title": "确认首页信息密度",
277
+ "status": "question",
278
+ "preview": "时钟、天气与通知的层级是否合适?",
279
+ "turn": 2,
280
+ "lastActiveAt": ago(70),
281
+ "pending": {
282
+ "kind": "question",
283
+ "requestId": "q-1",
284
+ "answerable": True,
285
+ "questions": [
286
+ {
287
+ "id": "q-1-1",
288
+ "header": "首页",
289
+ "question": "首页右侧的通知区域是否保留完整列表?",
290
+ "options": [
291
+ {"label": "保留完整列表(推荐)", "description": "可以滚动查看全部通知,信息更完整。"},
292
+ {"label": "只显示前三条", "description": "视觉更轻,但会隐藏部分通知。"},
293
+ {"label": "移到独立页面", "description": "首页更专注,但需要多一次跳转。"},
294
+ ],
295
+ }
296
+ ],
297
+ },
298
+ },
299
+ {
300
+ "id": "s-done",
301
+ "title": "整理演示数据",
302
+ "status": "done",
303
+ "preview": "已生成用于展示的示例内容。",
304
+ "turn": 11,
305
+ "lastActiveAt": ago(4 * 60 * 60),
306
+ },
307
+ ]
308
+
309
+ performance = {
310
+ "cpu": {
311
+ "usage": 11,
312
+ "model": "AMD Ryzen 7 5700X3D 8-Core Processor",
313
+ "cores": 16,
314
+ "physicalCores": 8,
315
+ "speed": 3810,
316
+ "uptime": 23840,
317
+ },
318
+ "memory": {"total": 17179869184, "used": 9930014720, "free": 7249854464, "percent": 58},
319
+ "gpu": {
320
+ "ok": True,
321
+ "name": "AMD Radeon RX 7800 XT",
322
+ "utilization": 2,
323
+ "memoryUsed": 2791728742,
324
+ "memoryTotal": 17179869184,
325
+ "memoryPercent": 16,
326
+ "coreClockMhz": 231,
327
+ "memoryClockMhz": 2418,
328
+ "driver": "32.0.23033.1002",
329
+ },
330
+ "temperatures": [
331
+ {"name": "AMD Radeon RX 7800 XT", "celsius": 57.0},
332
+ {"name": "磁盘 0", "celsius": 41.0},
333
+ {"name": "磁盘 1", "celsius": 47.0},
334
+ ],
335
+ "backend": "demo",
336
+ "sampledAt": int(time.time() * 1000),
337
+ }
338
+
339
+ media = {
340
+ "ok": True,
341
+ "media": {
342
+ "media": {
343
+ "appId": "spotify",
344
+ "appName": "Spotify",
345
+ "title": "Midnight City",
346
+ "artist": "M83",
347
+ "albumArtist": "M83",
348
+ "album": "Hurry Up, We're Dreaming",
349
+ "playbackStatus": "Playing",
350
+ "position": 96,
351
+ "startTime": 0,
352
+ "endTime": 244,
353
+ "canSeek": True,
354
+ "positionReported": True,
355
+ "canPlayPause": True,
356
+ "canNext": True,
357
+ "canPrevious": True,
358
+ "liked": True,
359
+ "canLike": True,
360
+ "canSetLiked": True,
361
+ "thumbnail": "/mock-cover.svg",
362
+ "thumbnailUrl": None,
363
+ },
364
+ "error": None,
365
+ },
366
+ "volume": {"volume": 0.62, "muted": False},
367
+ }
368
+
369
+ overview = {
370
+ "settings": settings,
371
+ "weather": weather,
372
+ "notifications": notifications,
373
+ "account": account,
374
+ "sessions": sessions,
375
+ "sessionsError": None,
376
+ "performance": performance,
377
+ }
378
+
379
+ messages = [
380
+ {"type": "welcome", "settings": settings},
381
+ {"type": "overview", "data": overview},
382
+ {"type": "media", "data": media},
383
+ {"type": "performance", "data": performance},
384
+ {"type": "clients", "data": []},
385
+ ]
386
+ return translate_demo(messages, locale)
387
+
388
+
389
+ def mock_socket_script(messages: list[dict]) -> str:
390
+ return (
391
+ """
392
+ (function () {
393
+ const params = new URLSearchParams(location.search);
394
+ const wantSheet = params.get('sheet') === '1';
395
+ const MESSAGES = __MESSAGES__;
396
+ class MockSocket {
397
+ static CONNECTING = 0;
398
+ static OPEN = 1;
399
+ static CLOSING = 2;
400
+ static CLOSED = 3;
401
+ constructor(url) {
402
+ this.url = url;
403
+ this.readyState = 0;
404
+ this._listeners = { open: [], message: [], close: [], error: [] };
405
+ setTimeout(() => {
406
+ this.readyState = 1;
407
+ this._emit('open', {});
408
+ for (const message of MESSAGES) this._emit('message', { data: JSON.stringify(message) });
409
+ if (wantSheet) {
410
+ setTimeout(() => {
411
+ const state = window.__DSH_REMOTE__;
412
+ if (state) state.answerSessionId = 's-question';
413
+ }, 700);
414
+ }
415
+ }, 40);
416
+ }
417
+ addEventListener(type, fn) {
418
+ (this._listeners[type] ||= []).push(fn);
419
+ }
420
+ removeEventListener(type, fn) {
421
+ this._listeners[type] = (this._listeners[type] || []).filter((item) => item !== fn);
422
+ }
423
+ send() {}
424
+ close() {
425
+ this.readyState = 3;
426
+ this._emit('close', {});
427
+ }
428
+ _emit(type, event) {
429
+ for (const fn of this._listeners[type] || []) fn(event);
430
+ }
431
+ }
432
+ window.WebSocket = MockSocket;
433
+ })();
434
+ """
435
+ ).replace("__MESSAGES__", json.dumps(messages, ensure_ascii=False))
436
+
437
+
438
+ def cover_svg() -> str:
439
+ dots = []
440
+ for index in range(120):
441
+ angle = (index * 2.399963) % (math.pi * 2)
442
+ radius = (index * 37) % 390 + 12
443
+ x = 450 + math.cos(angle) * radius
444
+ y = 450 + math.sin(angle) * radius
445
+ opacity = 0.10 + ((index * 7) % 20) / 100
446
+ dots.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{(index % 3) + 0.4:.1f}" fill="#dce8ff" opacity="{opacity:.2f}"/>')
447
+ return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900">
448
+ <defs>
449
+ <linearGradient id="base" x1="0" y1="0" x2="1" y2="1">
450
+ <stop offset="0" stop-color="#0b1730"/>
451
+ <stop offset="0.52" stop-color="#17306b"/>
452
+ <stop offset="1" stop-color="#0b2f63"/>
453
+ </linearGradient>
454
+ <radialGradient id="glow" cx="30%" cy="26%" r="72%">
455
+ <stop offset="0" stop-color="#819dff" stop-opacity="0.64"/>
456
+ <stop offset="0.56" stop-color="#2f5ecb" stop-opacity="0.18"/>
457
+ <stop offset="1" stop-color="#071426" stop-opacity="0"/>
458
+ </radialGradient>
459
+ <filter id="soft"><feGaussianBlur stdDeviation="38"/></filter>
460
+ </defs>
461
+ <rect width="900" height="900" fill="url(#base)"/>
462
+ <circle cx="280" cy="220" r="330" fill="url(#glow)" filter="url(#soft)"/>
463
+ <circle cx="770" cy="760" r="230" fill="#3d77ff" opacity="0.12" filter="url(#soft)"/>
464
+ {''.join(dots)}
465
+ </svg>
466
+ """
467
+
468
+
469
+ def freeze_script() -> str:
470
+ fixed = int(time.time() * 1000) + 5000
471
+ return f"""<script>/* FIXED_CLOCK */
472
+ (() => {{
473
+ const RealDate = Date;
474
+ const fixed = {fixed};
475
+ setTimeout(() => {{
476
+ class FixedDate extends RealDate {{
477
+ constructor(...args) {{ if (args.length) super(...args); else super(fixed); }}
478
+ static now() {{ return fixed; }}
479
+ }}
480
+ FixedDate.parse = RealDate.parse;
481
+ FixedDate.UTC = RealDate.UTC;
482
+ window.Date = FixedDate;
483
+ }}, 1500);
484
+ }})();
485
+ </script>"""
486
+
487
+
488
+ def prepare(
489
+ web_dir: Path,
490
+ out_dir: Path,
491
+ locale: str = "zh",
492
+ timer: str = "pomodoro",
493
+ theme: str = "dark",
494
+ freeze_clock: bool = False,
495
+ ) -> None:
496
+ if not (web_dir / "index.html").is_file():
497
+ raise SystemExit(f"missing built web page: {web_dir / 'index.html'}")
498
+ if out_dir.exists():
499
+ shutil.rmtree(out_dir)
500
+ shutil.copytree(web_dir, out_dir)
501
+ index = (out_dir / "index.html").read_text(encoding="utf-8")
502
+ injection = (
503
+ "<script>\n"
504
+ + mock_socket_script(demo_payloads(locale, timer, theme))
505
+ + "\n</script>\n"
506
+ + '<img src="/slow.png" alt="" aria-hidden="true" style="position:fixed;width:1px;height:1px;opacity:0;pointer-events:none">\n'
507
+ )
508
+ if freeze_clock:
509
+ injection += freeze_script() + "\n"
510
+ if "</head>" not in index:
511
+ raise SystemExit("index.html has no </head>")
512
+ index = index.replace("</head>", injection + "</head>", 1)
513
+ (out_dir / "index.html").write_text(index, encoding="utf-8")
514
+ (out_dir / "mock-cover.svg").write_text(cover_svg(), encoding="utf-8")
515
+
516
+
517
+ class PreviewHandler(http.server.SimpleHTTPRequestHandler):
518
+ png = bytes.fromhex(
519
+ "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
520
+ "0000000d49444154789c6360000000020001e221bc330000000049454e44ae426082"
521
+ )
522
+
523
+ def do_GET(self) -> None:
524
+ if self.path.split("?", 1)[0] == "/slow.png":
525
+ time.sleep(1.3)
526
+ self.send_response(200)
527
+ self.send_header("Content-Type", "image/png")
528
+ self.send_header("Content-Length", str(len(self.png)))
529
+ self.send_header("Cache-Control", "no-store")
530
+ self.end_headers()
531
+ self.wfile.write(self.png)
532
+ return
533
+ super().do_GET()
534
+
535
+ def log_message(self, fmt: str, *args: object) -> None:
536
+ return
537
+
538
+
539
+ class PreviewServer(socketserver.ThreadingTCPServer):
540
+ allow_reuse_address = True
541
+ daemon_threads = True
542
+
543
+
544
+ def main() -> int:
545
+ parser = argparse.ArgumentParser(description=__doc__)
546
+ parser.add_argument("--web", default=str(ROOT / "web"), help="built Vite output directory")
547
+ parser.add_argument("--out", default=str(ROOT / ".mock-web"), help="prepared preview directory")
548
+ parser.add_argument("--host", default="127.0.0.1")
549
+ parser.add_argument("--port", type=int, default=4173)
550
+ parser.add_argument("--locale", choices=["zh", "en"], default="zh", help="demo console language")
551
+ parser.add_argument("--timer", choices=["pomodoro", "countdown"], default="pomodoro", help="timer card shown on the home page")
552
+ parser.add_argument("--theme", choices=["dark", "light"], default="dark", help="demo console theme")
553
+ parser.add_argument("--freeze-clock", action="store_true", help="freeze the rendered clock so dark/light captures align")
554
+ args = parser.parse_args()
555
+
556
+ web_dir = Path(args.web).resolve()
557
+ out_dir = Path(args.out).resolve()
558
+ prepare(web_dir, out_dir, args.locale, args.timer, args.theme, args.freeze_clock)
559
+ handler = functools.partial(PreviewHandler, directory=str(out_dir))
560
+ with PreviewServer((args.host, args.port), handler) as server:
561
+ print(f"mock preview: http://{args.host}:{args.port}/?page=home", flush=True)
562
+ print(f"locale: {args.locale}", flush=True)
563
+ print(f"theme: {args.theme}", flush=True)
564
+ print(f"timer: {args.timer}", flush=True)
565
+ print("question: ?page=home&sheet=1", flush=True)
566
+ try:
567
+ server.serve_forever()
568
+ except KeyboardInterrupt:
569
+ pass
570
+ return 0
571
+
572
+
573
+ if __name__ == "__main__":
574
+ sys.exit(main())
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """Compose real-device dark/light screen captures into README images.
3
+
4
+ Input files are produced by `screenshot-device.sh` (or any other tool that
5
+ captures the whole device screen, never a headless browser viewport):
6
+
7
+ <input>/<prefix>dark-home.png
8
+ <input>/<prefix>light-home.png
9
+ ... sessions / music / performance / question
10
+
11
+ Output:
12
+
13
+ <output>/pages/<page>.png diagonal per-page composite
14
+ <output>/overview.png 2x2 overview with transparent gaps
15
+ <output>/question.png diagonal question composite
16
+
17
+ Every composite keeps the dark theme above the top-right-to-bottom-left
18
+ diagonal and the light theme below it.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ from pathlib import Path
25
+
26
+ from PIL import Image, ImageDraw
27
+
28
+ PAGES = ("home", "sessions", "music", "performance", "question")
29
+ OVERVIEW = ("home", "sessions", "music", "performance")
30
+
31
+
32
+ def diagonal(dark_path: Path, light_path: Path) -> Image.Image:
33
+ dark = Image.open(dark_path).convert("RGB")
34
+ light = Image.open(light_path).convert("RGB")
35
+ if dark.size != light.size:
36
+ raise SystemExit(f"size mismatch: {dark_path} is {dark.size}, {light_path} is {light.size}")
37
+
38
+ width, height = dark.size
39
+ scale = 4
40
+ mask = Image.new("L", (width * scale, height * scale), 0)
41
+ ImageDraw.Draw(mask).polygon(
42
+ [(0, 0), (width * scale, 0), (0, height * scale)],
43
+ fill=255,
44
+ )
45
+ mask = mask.resize((width, height), Image.Resampling.LANCZOS)
46
+ return Image.composite(dark, light, mask)
47
+
48
+
49
+ def main() -> int:
50
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
51
+ parser.add_argument("--input", required=True, help="directory containing dark-*.png / light-*.png")
52
+ parser.add_argument("--output", required=True, help="directory for overview.png, question.png and pages/")
53
+ parser.add_argument("--prefix", default="", help="input filename prefix, e.g. en-")
54
+ parser.add_argument("--gap", type=int, default=24, help="transparent gap between overview tiles")
55
+ args = parser.parse_args()
56
+
57
+ source = Path(args.input)
58
+ target = Path(args.output)
59
+ pages_dir = target / "pages"
60
+ pages_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ def composite(page: str) -> Image.Image:
63
+ dark = source / f"{args.prefix}dark-{page}.png"
64
+ light = source / f"{args.prefix}light-{page}.png"
65
+ if not dark.is_file() or not light.is_file():
66
+ raise SystemExit(f"missing capture for {page}: {dark} / {light}")
67
+ image = diagonal(dark, light)
68
+ image.save(pages_dir / f"{page}.png", optimize=True)
69
+ return image
70
+
71
+ tiles = {page: composite(page) for page in PAGES}
72
+
73
+ width, height = tiles["home"].size
74
+ overview = Image.new("RGBA", (width * 2 + args.gap, height * 2 + args.gap), (0, 0, 0, 0))
75
+ overview.paste(tiles["home"], (0, 0))
76
+ overview.paste(tiles["sessions"], (width + args.gap, 0))
77
+ overview.paste(tiles["music"], (0, height + args.gap))
78
+ overview.paste(tiles["performance"], (width + args.gap, height + args.gap))
79
+ overview.save(target / "overview.png", optimize=True)
80
+
81
+ tiles["question"].convert("RGBA").save(target / "question.png", optimize=True)
82
+ print(f"wrote {target / 'overview.png'} ({overview.width}x{overview.height})")
83
+ print(f"wrote {target / 'question.png'}")
84
+ print(f"wrote {pages_dir}/{{{','.join(PAGES)}}}.png")
85
+ return 0
86
+
87
+
88
+ if __name__ == "__main__":
89
+ raise SystemExit(main())