@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,51 @@
1
+ <script lang="ts">
2
+ import { Tooltip as TooltipPrimitive } from "bits-ui";
3
+ import { cn } from "$lib/utils.js";
4
+ import type { WithoutChildrenOrChild } from "$lib/utils.js";
5
+ import TooltipPortal from "./tooltip-portal.svelte";
6
+ import type { ComponentProps } from "svelte";
7
+
8
+ let {
9
+ ref = $bindable(null),
10
+ class: className,
11
+ sideOffset = 0,
12
+ side = "top",
13
+ children,
14
+ arrowClasses,
15
+ portalProps,
16
+ ...restProps
17
+ }: TooltipPrimitive.ContentProps & {
18
+ arrowClasses?: string;
19
+ portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
20
+ } = $props();
21
+ </script>
22
+
23
+ <TooltipPortal {...portalProps}>
24
+ <TooltipPrimitive.Content
25
+ bind:ref
26
+ data-slot="tooltip-content"
27
+ {sideOffset}
28
+ {side}
29
+ class={cn(
30
+ "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm z-50 w-fit max-w-xs origin-(--bits-tooltip-content-transform-origin) bg-foreground text-background",
31
+ className
32
+ )}
33
+ {...restProps}
34
+ >
35
+ {@render children?.()}
36
+ <TooltipPrimitive.Arrow>
37
+ {#snippet child({ props })}
38
+ <!-- Like Radix: the wrapper takes the popper transforms, only the inner square rotates. -->
39
+ <div {...props}>
40
+ <div
41
+ class={cn(
42
+ "size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] z-50 translate-y-[calc(-50%_-_2px)] bg-foreground fill-foreground",
43
+ arrowClasses
44
+ )}
45
+ data-side={props["data-side"]}
46
+ ></div>
47
+ </div>
48
+ {/snippet}
49
+ </TooltipPrimitive.Arrow>
50
+ </TooltipPrimitive.Content>
51
+ </TooltipPortal>
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Tooltip as TooltipPrimitive } from "bits-ui";
3
+
4
+ let { ...restProps }: TooltipPrimitive.PortalProps = $props();
5
+ </script>
6
+
7
+ <TooltipPrimitive.Portal {...restProps} />
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Tooltip as TooltipPrimitive } from "bits-ui";
3
+
4
+ let { delayDuration = 0, ...restProps }: TooltipPrimitive.ProviderProps = $props();
5
+ </script>
6
+
7
+ <TooltipPrimitive.Provider {delayDuration} {...restProps} />
@@ -0,0 +1,7 @@
1
+ <script lang="ts" generics="T = never">
2
+ import { Tooltip as TooltipPrimitive } from "bits-ui";
3
+
4
+ let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps<T> = $props();
5
+ </script>
6
+
7
+ <TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
@@ -0,0 +1,7 @@
1
+ <script lang="ts" generics="T = never">
2
+ import { Tooltip as TooltipPrimitive } from "bits-ui";
3
+
4
+ let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps<T> = $props();
5
+ </script>
6
+
7
+ <TooltipPrimitive.Root bind:open {...restProps} />
@@ -0,0 +1,252 @@
1
+ import {
2
+ cancelScheduledAlarm,
3
+ claimScheduledAlarm,
4
+ primeAudio,
5
+ scheduleAlarm,
6
+ setTabAlert,
7
+ startLoopingAlarm,
8
+ stopAlarmSound,
9
+ } from './timer-alarm.svelte';
10
+ import { app } from './state.svelte';
11
+ import type { CountdownSettings } from './types';
12
+
13
+ /**
14
+ * Countdown timer that lives entirely in the controller's browser.
15
+ *
16
+ * The DSH panel only persists the enabled flag and the preset list; this module
17
+ * owns the run. Timing derives from an absolute `endsAt`, so switching console
18
+ * pages never loses time. There is no loop option: once the time is up the
19
+ * alarm keeps ringing until the console user confirms it.
20
+ */
21
+
22
+ export const COUNTDOWN_MAX_SECONDS = 86_400;
23
+
24
+ export interface CountdownRuntime {
25
+ /** Index into the configured preset list. */
26
+ presetIndex: number;
27
+ /** Duration of the current run, kept stable while the timer advances. */
28
+ durationMs: number;
29
+ running: boolean;
30
+ remainingMs: number;
31
+ endsAt: number;
32
+ /** Time is up and the alarm is waiting for confirmation. */
33
+ finished: boolean;
34
+ alarmActive: boolean;
35
+ }
36
+
37
+ export const countdown = $state<CountdownRuntime>({
38
+ presetIndex: 0,
39
+ durationMs: 60_000,
40
+ running: false,
41
+ remainingMs: 60_000,
42
+ endsAt: 0,
43
+ finished: false,
44
+ alarmActive: false,
45
+ });
46
+
47
+ let refs = 0;
48
+ let timer: ReturnType<typeof setInterval> | null = null;
49
+ let presetSignature = '';
50
+
51
+ function settings(): CountdownSettings {
52
+ return app.countdown;
53
+ }
54
+
55
+ function clampPresetIndex(index: number): number {
56
+ const count = settings().presets.length;
57
+ if (count <= 0) return 0;
58
+ const value = Math.round(Number(index));
59
+ if (!Number.isFinite(value)) return 0;
60
+ return Math.max(0, Math.min(count - 1, value));
61
+ }
62
+
63
+ /** Seconds configured for one preset, clamped to the accepted range. */
64
+ export function presetSeconds(index: number = countdown.presetIndex): number {
65
+ const value = Number(settings().presets[clampPresetIndex(index)]);
66
+ const seconds = Number.isFinite(value) ? Math.round(value) : 60;
67
+ return Math.max(1, Math.min(COUNTDOWN_MAX_SECONDS, seconds));
68
+ }
69
+
70
+ export function presetDurationMs(index: number = countdown.presetIndex): number {
71
+ return presetSeconds(index) * 1000;
72
+ }
73
+
74
+ /** `m:ss`, or `h:mm:ss` once the duration reaches an hour. */
75
+ export function formatSeconds(totalSeconds: number): string {
76
+ const value = Math.max(0, Math.round(totalSeconds));
77
+ const hours = Math.floor(value / 3600);
78
+ const minutes = Math.floor((value % 3600) / 60);
79
+ const seconds = value % 60;
80
+ const pad = (part: number): string => String(part).padStart(2, '0');
81
+ if (hours > 0) return `${hours}:${pad(minutes)}:${pad(seconds)}`;
82
+ return `${pad(minutes)}:${pad(seconds)}`;
83
+ }
84
+
85
+ export function formatCountdown(ms: number): string {
86
+ return formatSeconds(Math.ceil(Math.max(0, ms) / 1000));
87
+ }
88
+
89
+ export function countdownProgress(): number {
90
+ const duration = countdown.durationMs;
91
+ if (duration <= 0) return 0;
92
+ return Math.max(0, Math.min(100, ((duration - countdown.remainingMs) / duration) * 100));
93
+ }
94
+
95
+ function syncPresets(): void {
96
+ const presets = settings().presets;
97
+ const signature = presets.join('/');
98
+ if (signature === presetSignature) return;
99
+ presetSignature = signature;
100
+ countdown.presetIndex = clampPresetIndex(countdown.presetIndex);
101
+ if (!countdown.running && !countdown.finished) {
102
+ countdown.durationMs = presetDurationMs();
103
+ countdown.remainingMs = countdown.durationMs;
104
+ }
105
+ }
106
+
107
+ function resetRuntime(): void {
108
+ countdown.presetIndex = clampPresetIndex(countdown.presetIndex);
109
+ countdown.running = false;
110
+ countdown.endsAt = 0;
111
+ countdown.finished = false;
112
+ countdown.durationMs = presetDurationMs();
113
+ countdown.remainingMs = countdown.durationMs;
114
+ countdown.alarmActive = false;
115
+ stopAlarmSound();
116
+ }
117
+
118
+ function startEngine(): void {
119
+ presetSignature = settings().presets.join('/');
120
+ countdown.presetIndex = clampPresetIndex(countdown.presetIndex);
121
+ if (!countdown.running && !countdown.finished) {
122
+ countdown.durationMs = presetDurationMs();
123
+ countdown.remainingMs = countdown.durationMs;
124
+ }
125
+ if (timer) return;
126
+ timer = setInterval(tick, 250);
127
+ document.addEventListener('visibilitychange', handleVisibility);
128
+ (window as unknown as { __DSH_COUNTDOWN__?: CountdownRuntime }).__DSH_COUNTDOWN__ = countdown;
129
+ }
130
+
131
+ function stopEngine(): void {
132
+ if (timer) clearInterval(timer);
133
+ timer = null;
134
+ document.removeEventListener('visibilitychange', handleVisibility);
135
+ cancelScheduledAlarm();
136
+ resetRuntime();
137
+ }
138
+
139
+ function handleVisibility(): void {
140
+ if (!document.hidden) tick();
141
+ }
142
+
143
+ function tick(): void {
144
+ if (!settings().enabled) {
145
+ resetRuntime();
146
+ return;
147
+ }
148
+ syncPresets();
149
+ if (!countdown.running) return;
150
+ const remaining = countdown.endsAt - Date.now();
151
+ if (remaining <= 0) {
152
+ countdown.remainingMs = 0;
153
+ finish();
154
+ return;
155
+ }
156
+ countdown.remainingMs = remaining;
157
+ }
158
+
159
+ /** Reference-counted lifecycle; the card (and its clone) both acquire it. */
160
+ export function acquireCountdown(): () => void {
161
+ refs += 1;
162
+ if (refs === 1) startEngine();
163
+ let released = false;
164
+ return () => {
165
+ if (released) return;
166
+ released = true;
167
+ refs = Math.max(0, refs - 1);
168
+ if (refs === 0) stopEngine();
169
+ };
170
+ }
171
+
172
+ /** Quick-select one preset while the timer is idle or paused. */
173
+ export function selectCountdownPreset(index: number): void {
174
+ if (!settings().enabled || countdown.running || countdown.alarmActive) return;
175
+ countdown.presetIndex = clampPresetIndex(index);
176
+ countdown.finished = false;
177
+ countdown.running = false;
178
+ countdown.endsAt = 0;
179
+ countdown.durationMs = presetDurationMs();
180
+ countdown.remainingMs = countdown.durationMs;
181
+ stopAlarmSound();
182
+ }
183
+
184
+ export function startCountdown(): void {
185
+ if (!settings().enabled || countdown.alarmActive) return;
186
+ primeAudio();
187
+ if (countdown.finished) {
188
+ countdown.finished = false;
189
+ countdown.durationMs = presetDurationMs();
190
+ countdown.remainingMs = countdown.durationMs;
191
+ }
192
+ if (countdown.remainingMs <= 0) countdown.remainingMs = countdown.durationMs;
193
+ countdown.running = true;
194
+ countdown.endsAt = Date.now() + countdown.remainingMs;
195
+ stopAlarmSound();
196
+ if (!app.pomodoro.soundPath.trim()) {
197
+ scheduleAlarm(Math.max(0.06, (countdown.endsAt - Date.now()) / 1000), countdown.endsAt);
198
+ }
199
+ }
200
+
201
+ export function pauseCountdown(): void {
202
+ if (!countdown.running) return;
203
+ countdown.remainingMs = Math.max(0, countdown.endsAt - Date.now());
204
+ countdown.running = false;
205
+ countdown.endsAt = 0;
206
+ cancelScheduledAlarm();
207
+ }
208
+
209
+ export function toggleCountdown(): void {
210
+ if (countdown.running) pauseCountdown();
211
+ else startCountdown();
212
+ }
213
+
214
+ export function resetCountdown(): void {
215
+ countdown.running = false;
216
+ countdown.finished = false;
217
+ countdown.endsAt = 0;
218
+ countdown.durationMs = presetDurationMs();
219
+ countdown.remainingMs = countdown.durationMs;
220
+ countdown.alarmActive = false;
221
+ stopAlarmSound();
222
+ }
223
+
224
+ /**
225
+ * Acknowledge the finished countdown. The alarm never stops on its own: this
226
+ * is the confirmation the phone user has to give.
227
+ */
228
+ export function confirmCountdown(): void {
229
+ if (!countdown.alarmActive && !countdown.finished) return;
230
+ stopAlarmSound();
231
+ countdown.alarmActive = false;
232
+ countdown.finished = false;
233
+ countdown.running = false;
234
+ countdown.endsAt = 0;
235
+ countdown.durationMs = presetDurationMs();
236
+ countdown.remainingMs = countdown.durationMs;
237
+ }
238
+
239
+ function finish(): void {
240
+ countdown.running = false;
241
+ countdown.remainingMs = 0;
242
+ countdown.finished = true;
243
+ countdown.alarmActive = true;
244
+ const hasScheduledChime = claimScheduledAlarm(countdown.endsAt);
245
+ startLoopingAlarm(app.pomodoro.soundPath, hasScheduledChime);
246
+ setTabAlert(true);
247
+ try {
248
+ navigator.vibrate?.([140, 90, 180]);
249
+ } catch {
250
+ /* vibration is optional */
251
+ }
252
+ }
@@ -0,0 +1,199 @@
1
+ import type { TemperatureUnit } from './types';
2
+ import { isChinese, localeTag, t, type MessageKey } from './i18n.svelte';
3
+
4
+ /** Shared formatting helpers, ported from the original web/app.js. */
5
+
6
+ export function pad(value: string | number): string {
7
+ return String(value).padStart(2, '0');
8
+ }
9
+
10
+ export function timeAgo(timestamp: number | undefined, now = Date.now()): string {
11
+ if (!Number.isFinite(timestamp)) return '';
12
+ const seconds = Math.max(0, Math.round((now - (timestamp as number)) / 1000));
13
+ if (seconds < 45) return t('justNow');
14
+ if (seconds < 3600) return t('minutesAgo', { count: Math.floor(seconds / 60) });
15
+ if (seconds < 86400) return t('hoursAgo', { count: Math.floor(seconds / 3600) });
16
+ return t('daysAgo', { count: Math.floor(seconds / 86400) });
17
+ }
18
+
19
+ const LUNAR_DAYS = ['初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'];
20
+
21
+ const SOLAR_TERM_KEYS: readonly MessageKey[] = [
22
+ 'termMinorCold', 'termMajorCold', 'termStartOfSpring', 'termRainWater', 'termAwakening', 'termSpringEquinox',
23
+ 'termClearAndBright', 'termGrainRain', 'termStartOfSummer', 'termGrainFull', 'termGrainInEar', 'termSummerSolstice',
24
+ 'termMinorHeat', 'termMajorHeat', 'termStartOfAutumn', 'termEndOfHeat', 'termWhiteDew', 'termAutumnEquinox',
25
+ 'termColdDew', 'termFrostDescent', 'termStartOfWinter', 'termMinorSnow', 'termMajorSnow', 'termWinterSolstice',
26
+ ];
27
+
28
+ const SOLAR_TERM_MINUTES = [0, 21208, 42467, 63836, 85337, 107014, 128867, 150921, 173149, 195551, 218072, 240693, 263343, 285989, 308563, 331033, 353350, 375494, 397447, 419210, 440795, 462224, 483532, 504758];
29
+
30
+ let lunarFormatter: Intl.DateTimeFormat | null = null;
31
+ function getLunarFormatter(): Intl.DateTimeFormat | null {
32
+ if (lunarFormatter) return lunarFormatter;
33
+ try {
34
+ lunarFormatter = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', { month: 'long', day: 'numeric' });
35
+ } catch {
36
+ lunarFormatter = null;
37
+ }
38
+ return lunarFormatter;
39
+ }
40
+
41
+ /** Lunar month/day text; the Chinese calendar labels only make sense in zh. */
42
+ export function lunarDateText(date: Date): string {
43
+ if (!isChinese()) return '';
44
+ const formatter = getLunarFormatter();
45
+ if (!formatter) return '';
46
+ try {
47
+ const parts = formatter.formatToParts(date);
48
+ const month = parts.find((part) => part.type === 'month')?.value || '';
49
+ const day = Number(parts.find((part) => part.type === 'day')?.value);
50
+ if (!month || !Number.isInteger(day) || day < 1 || day > 30) return '';
51
+ return `${month}${LUNAR_DAYS[day - 1]}`;
52
+ } catch {
53
+ return '';
54
+ }
55
+ }
56
+
57
+ /** Which of the 24 solar terms falls on the given date, if any. */
58
+ export function solarTermForDate(date: Date): MessageKey | null {
59
+ const year = date.getFullYear();
60
+ const month = date.getMonth();
61
+ const day = date.getDate();
62
+ for (let index = 0; index < SOLAR_TERM_KEYS.length; index += 1) {
63
+ const term = new Date(31556925974.7 * (year - 1900) + SOLAR_TERM_MINUTES[index] * 60000 + Date.UTC(1900, 0, 6, 2, 5));
64
+ if (term.getUTCFullYear() === year && term.getUTCMonth() === month && term.getUTCDate() === day) {
65
+ return SOLAR_TERM_KEYS[index];
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export interface ClockParts {
72
+ time: string;
73
+ seconds: string;
74
+ greeting: string;
75
+ periodLabel: string;
76
+ dateLabel: string;
77
+ }
78
+
79
+ export function clockParts(date: Date): ClockParts {
80
+ const hour = date.getHours();
81
+ let greeting = 'greetingEvening';
82
+ let period = 'periodEvening';
83
+ if (hour >= 5 && hour < 11) {
84
+ greeting = 'greetingMorning';
85
+ period = 'periodMorning';
86
+ } else if (hour >= 11 && hour < 13) {
87
+ greeting = 'greetingNoon';
88
+ period = 'periodNoon';
89
+ } else if (hour >= 13 && hour < 18) {
90
+ greeting = 'greetingAfternoon';
91
+ period = 'periodAfternoon';
92
+ }
93
+ const lunar = lunarDateText(date);
94
+ const solarTerm = solarTermForDate(date);
95
+ // Chinese keeps lunar/solar-term detail with a period fallback; English shows
96
+ // the solar term when present and nothing otherwise, so the clock has no
97
+ // placeholder line like "Afternoon".
98
+ const periodLabel = isChinese()
99
+ ? ([lunar, solarTerm ? t(solarTerm) : ''].filter(Boolean).join(' · ') || t(period as MessageKey))
100
+ : (solarTerm ? t(solarTerm) : '');
101
+ return {
102
+ time: `${pad(hour)}:${pad(date.getMinutes())}`,
103
+ seconds: pad(date.getSeconds()),
104
+ greeting: t(greeting as MessageKey),
105
+ periodLabel,
106
+ dateLabel: new Intl.DateTimeFormat(localeTag(), { month: 'long', day: 'numeric', weekday: 'short' }).format(date),
107
+ };
108
+ }
109
+
110
+ export function formatMoney(value: unknown, currency = 'CNY'): string {
111
+ if (value === null || value === undefined || value === '' || !Number.isFinite(Number(value))) return '—';
112
+ const symbol = String(currency || '').toUpperCase() === 'USD' ? '$' : '¥';
113
+ const abs = Math.abs(Number(value));
114
+ const digits = abs > 0 && abs < 1 ? 4 : 2;
115
+ return `${symbol}${Number(value).toFixed(digits).replace(/\.?0+$/, '')}`;
116
+ }
117
+
118
+ export function currencySymbol(currency?: string): string {
119
+ return String(currency || '').toUpperCase() === 'USD' ? '$' : '¥';
120
+ }
121
+
122
+ export function formatDuration(seconds: unknown): string {
123
+ const total = Math.max(0, Math.round(Number(seconds) || 0));
124
+ return `${Math.floor(total / 60)}:${pad(total % 60)}`;
125
+ }
126
+
127
+ /** Elapsed time since the open DSH turn started (`h:mm:ss` past an hour). */
128
+ export function runningDuration(since: number | null | undefined, now = Date.now()): string {
129
+ if (!Number.isFinite(since)) return '';
130
+ const total = Math.max(0, Math.floor((now - (since as number)) / 1000));
131
+ const hours = Math.floor(total / 3600);
132
+ const minutes = Math.floor((total % 3600) / 60);
133
+ const seconds = total % 60;
134
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
135
+ }
136
+
137
+ export function formatBytes(value: unknown): string {
138
+ const bytes = Number(value);
139
+ if (!Number.isFinite(bytes) || bytes <= 0) return '—';
140
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
141
+ let index = 0;
142
+ let size = bytes;
143
+ while (size >= 1024 && index < units.length - 1) {
144
+ size /= 1024;
145
+ index += 1;
146
+ }
147
+ return `${size >= 10 || index === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[index]}`;
148
+ }
149
+
150
+ export function formatUptime(seconds: unknown): string {
151
+ const total = Math.max(0, Number(seconds) || 0);
152
+ const days = Math.floor(total / 86400);
153
+ const hours = Math.floor((total % 86400) / 3600);
154
+ const minutes = Math.floor((total % 3600) / 60);
155
+ if (days > 0) return t('uptimeDays', { days, hours });
156
+ if (hours > 0) return t('uptimeHours', { hours, minutes });
157
+ return t('uptimeMinutes', { minutes });
158
+ }
159
+
160
+ export function appInitial(name?: string, title?: string): string {
161
+ return Array.from(String(name || title || '?').trim())[0]?.toUpperCase() || '?';
162
+ }
163
+
164
+ export function appGradient(key?: string): string {
165
+ let hash = 0;
166
+ for (const char of String(key || 'app')) hash = (hash * 31 + char.charCodeAt(0)) % 360;
167
+ return `linear-gradient(145deg, hsl(${hash} 45% 30%), hsl(${(hash + 46) % 360} 48% 20%))`;
168
+ }
169
+
170
+ export function sourceLabel(appId?: string): string {
171
+ const id = String(appId || '');
172
+ if (/bilibili/i.test(id)) return t('sourceBilibili');
173
+ if (/spotify/i.test(id)) return 'Spotify';
174
+ if (/msedge/i.test(id)) return 'Edge';
175
+ if (/brave/i.test(id)) return 'Brave';
176
+ if (/vivaldi/i.test(id)) return 'Vivaldi';
177
+ if (/firefox/i.test(id)) return 'Firefox';
178
+ if (/Music/i.test(id)) return t('musicSource');
179
+ if (/chrome/i.test(id)) return 'Chrome';
180
+ if (id === 'macOS') return 'macOS';
181
+ if (id.startsWith('org.mpris.MediaPlayer2.')) return id.replace('org.mpris.MediaPlayer2.', '').replace(/[._]/g, ' ');
182
+ return id.split('.').pop()?.replace(/[._-]/g, ' ') || t('mediaFallback');
183
+ }
184
+
185
+ export function temperatureUnitLabel(unit: TemperatureUnit): string {
186
+ return unit === 'fahrenheit' ? '°F' : '°C';
187
+ }
188
+
189
+ export function toDisplayTemperature(value: unknown, unit: TemperatureUnit): number | null {
190
+ const number = Number(value);
191
+ if (!Number.isFinite(number)) return null;
192
+ return unit === 'fahrenheit' ? (number * 9) / 5 + 32 : number;
193
+ }
194
+
195
+ export function formatTemperature(value: unknown, unit: TemperatureUnit, digits = 0): string {
196
+ const number = toDisplayTemperature(value, unit);
197
+ if (number === null) return '--';
198
+ return `${digits > 0 ? number.toFixed(digits) : Math.round(number)}${temperatureUnitLabel(unit)}`;
199
+ }