@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.
- package/AGENTS.md +556 -0
- package/INSTALLATION.md +310 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/client.js +3 -0
- package/components.json +20 -0
- package/cordis.patch.yml +3 -0
- package/docs/README.en.md +196 -0
- package/docs/screenshots/en/overview.png +0 -0
- package/docs/screenshots/en/question.png +0 -0
- package/docs/screenshots/zh/overview.png +0 -0
- package/docs/screenshots/zh/question.png +0 -0
- package/icon.svg +24 -0
- package/index.js +2 -0
- package/lib/account.js +1 -0
- package/lib/app-mapping.js +1 -0
- package/lib/i18n.js +1 -0
- package/lib/media-enrich.js +1 -0
- package/lib/notifications.js +1 -0
- package/lib/performance.js +1 -0
- package/lib/platform/index.js +1 -0
- package/lib/probe-adapter.js +1 -0
- package/lib/probe-daemon.js +1 -0
- package/lib/probe.js +1 -0
- package/lib/qr.js +1 -0
- package/lib/runtime.js +1 -0
- package/lib/service.js +1 -0
- package/lib/sessions.js +1 -0
- package/lib/settings.js +1 -0
- package/lib/types.js +1 -0
- package/lib/util.js +1 -0
- package/lib/weather.js +1 -0
- package/lib/ws.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +118 -4
- package/scripts/browser-shot.mjs +217 -0
- package/scripts/build-dsh.mjs +228 -0
- package/scripts/build-rust.mjs +319 -0
- package/scripts/build-web.mjs +17 -0
- package/scripts/check-npm-package.mjs +322 -0
- package/scripts/mock-preview.py +574 -0
- package/scripts/screenshot-compose.py +89 -0
- package/scripts/screenshot-device.sh +75 -0
- package/src/dsh/client/api.ts +68 -0
- package/src/dsh/client/i18n.ts +573 -0
- package/src/dsh/client/icons.ts +56 -0
- package/src/dsh/client/index.ts +95 -0
- package/src/dsh/client/logic.ts +310 -0
- package/src/dsh/client/panel.ts +483 -0
- package/src/dsh/client/sections/apps.ts +40 -0
- package/src/dsh/client/sections/balance.ts +38 -0
- package/src/dsh/client/sections/connection.ts +97 -0
- package/src/dsh/client/sections/notifications.ts +43 -0
- package/src/dsh/client/sections/pomodoro.ts +184 -0
- package/src/dsh/client/sections/preferences.ts +138 -0
- package/src/dsh/client/sections/system.ts +120 -0
- package/src/dsh/client/sections/weather.ts +276 -0
- package/src/dsh/client/styles.ts +1119 -0
- package/src/dsh/client/types.ts +366 -0
- package/src/dsh/client/ui.ts +142 -0
- package/src/dsh/index.ts +108 -0
- package/src/dsh/lib/account.ts +520 -0
- package/src/dsh/lib/app-mapping.ts +366 -0
- package/src/dsh/lib/i18n.ts +82 -0
- package/src/dsh/lib/media-enrich.ts +202 -0
- package/src/dsh/lib/notifications.ts +322 -0
- package/src/dsh/lib/performance.ts +279 -0
- package/src/dsh/lib/platform/index.ts +43 -0
- package/src/dsh/lib/probe-adapter.ts +275 -0
- package/src/dsh/lib/probe-daemon.ts +359 -0
- package/src/dsh/lib/probe.ts +395 -0
- package/src/dsh/lib/qr.ts +42 -0
- package/src/dsh/lib/runtime.ts +126 -0
- package/src/dsh/lib/service.ts +1645 -0
- package/src/dsh/lib/sessions.ts +907 -0
- package/src/dsh/lib/settings.ts +468 -0
- package/src/dsh/lib/types.ts +231 -0
- package/src/dsh/lib/util.ts +115 -0
- package/src/dsh/lib/weather.ts +532 -0
- package/src/dsh/lib/ws.ts +328 -0
- package/src/dsh/types/qrcode-generator.d.ts +16 -0
- package/src/rust/Cargo.lock +1431 -0
- package/src/rust/Cargo.toml +68 -0
- package/src/rust/src/cli.rs +233 -0
- package/src/rust/src/daemon.rs +140 -0
- package/src/rust/src/location/linux.rs +118 -0
- package/src/rust/src/location/macos.rs +26 -0
- package/src/rust/src/location/mod.rs +170 -0
- package/src/rust/src/location/windows.rs +205 -0
- package/src/rust/src/main.rs +42 -0
- package/src/rust/src/media/catalog.rs +454 -0
- package/src/rust/src/media/linux.rs +350 -0
- package/src/rust/src/media/macos/accessibility.rs +59 -0
- package/src/rust/src/media/macos.rs +366 -0
- package/src/rust/src/media/mod.rs +298 -0
- package/src/rust/src/media/vendor/apple_music.rs +90 -0
- package/src/rust/src/media/vendor/common.rs +186 -0
- package/src/rust/src/media/vendor/kugou.rs +52 -0
- package/src/rust/src/media/vendor/kuwo.rs +49 -0
- package/src/rust/src/media/vendor/mod.rs +75 -0
- package/src/rust/src/media/vendor/netease.rs +1715 -0
- package/src/rust/src/media/vendor/qishui.rs +36 -0
- package/src/rust/src/media/vendor/qqmusic.rs +33 -0
- package/src/rust/src/media/vendor/spotify.rs +46 -0
- package/src/rust/src/media/vendor/wesing.rs +39 -0
- package/src/rust/src/media/vendor/youtube_music.rs +49 -0
- package/src/rust/src/media/windows/accessibility.rs +85 -0
- package/src/rust/src/media/windows/audio.rs +116 -0
- package/src/rust/src/media/windows/private.rs +291 -0
- package/src/rust/src/media/windows.rs +775 -0
- package/src/rust/src/notifications/linux.rs +276 -0
- package/src/rust/src/notifications/macos.rs +382 -0
- package/src/rust/src/notifications/mod.rs +244 -0
- package/src/rust/src/notifications/windows.rs +565 -0
- package/src/rust/src/perf/common.rs +109 -0
- package/src/rust/src/perf/linux.rs +282 -0
- package/src/rust/src/perf/macos.rs +28 -0
- package/src/rust/src/perf/mod.rs +157 -0
- package/src/rust/src/perf/windows.rs +981 -0
- package/src/rust/src/util.rs +360 -0
- package/src/rust/src/volume/linux.rs +195 -0
- package/src/rust/src/volume/macos.rs +54 -0
- package/src/rust/src/volume/mod.rs +116 -0
- package/src/rust/src/volume/windows.rs +75 -0
- package/src/rust/tests/cli.rs +79 -0
- package/src/rust/tests/daemon.rs +107 -0
- package/src/svelte/App.svelte +385 -0
- package/src/svelte/index.html +18 -0
- package/src/svelte/lib/billing.svelte.ts +228 -0
- package/src/svelte/lib/components/BalanceCard.svelte +173 -0
- package/src/svelte/lib/components/ClockCard.svelte +183 -0
- package/src/svelte/lib/components/CountdownCard.svelte +261 -0
- package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
- package/src/svelte/lib/components/EmptyState.svelte +34 -0
- package/src/svelte/lib/components/GpuCard.svelte +245 -0
- package/src/svelte/lib/components/HomePage.svelte +34 -0
- package/src/svelte/lib/components/MusicControls.svelte +256 -0
- package/src/svelte/lib/components/MusicCover.svelte +88 -0
- package/src/svelte/lib/components/MusicPage.svelte +116 -0
- package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
- package/src/svelte/lib/components/Panel.svelte +22 -0
- package/src/svelte/lib/components/PerformancePage.svelte +55 -0
- package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
- package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
- package/src/svelte/lib/components/RadialGauge.svelte +99 -0
- package/src/svelte/lib/components/Rail.svelte +217 -0
- package/src/svelte/lib/components/SectionHeader.svelte +40 -0
- package/src/svelte/lib/components/SeekBar.svelte +421 -0
- package/src/svelte/lib/components/SessionList.svelte +364 -0
- package/src/svelte/lib/components/SessionStats.svelte +98 -0
- package/src/svelte/lib/components/SessionsPage.svelte +61 -0
- package/src/svelte/lib/components/SpendCard.svelte +105 -0
- package/src/svelte/lib/components/ThermalCard.svelte +249 -0
- package/src/svelte/lib/components/Toast.svelte +32 -0
- package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
- package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
- package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
- package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
- package/src/svelte/lib/components/ui/alert/index.ts +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
- package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
- package/src/svelte/lib/components/ui/badge/index.ts +2 -0
- package/src/svelte/lib/components/ui/button/button.svelte +82 -0
- package/src/svelte/lib/components/ui/button/index.ts +17 -0
- package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card.svelte +22 -0
- package/src/svelte/lib/components/ui/card/index.ts +25 -0
- package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
- package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
- package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/index.ts +22 -0
- package/src/svelte/lib/components/ui/input/index.ts +7 -0
- package/src/svelte/lib/components/ui/input/input.svelte +48 -0
- package/src/svelte/lib/components/ui/progress/index.ts +7 -0
- package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
- package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
- package/src/svelte/lib/components/ui/separator/index.ts +7 -0
- package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
- package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
- package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
- package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
- package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/src/svelte/lib/components/ui/slider/index.ts +7 -0
- package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
- package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/svelte/lib/countdown.svelte.ts +252 -0
- package/src/svelte/lib/format.ts +199 -0
- package/src/svelte/lib/i18n.svelte.ts +583 -0
- package/src/svelte/lib/media.ts +32 -0
- package/src/svelte/lib/pages.ts +18 -0
- package/src/svelte/lib/pomodoro.svelte.ts +223 -0
- package/src/svelte/lib/session-tree.ts +80 -0
- package/src/svelte/lib/state.svelte.ts +359 -0
- package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
- package/src/svelte/lib/types.ts +306 -0
- package/src/svelte/lib/utils.ts +8 -0
- package/src/svelte/lib/weather.ts +84 -0
- package/src/svelte/lib/ws.ts +104 -0
- package/src/svelte/main.ts +13 -0
- package/src/svelte/styles/global.css +315 -0
- package/src/svelte/vite-env.d.ts +12 -0
- package/static/fonts/LICENSE.txt +287 -0
- package/static/icon.svg +24 -0
- package/static/manifest.webmanifest +18 -0
- package/svelte.config.js +5 -0
- package/tsconfig.dsh-client.json +18 -0
- package/tsconfig.dsh.json +21 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +31 -0
- package/web/assets/index-DLdSfnJ3.css +1 -0
- package/web/assets/index-YNE8X0Uy.js +6 -0
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
- package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
- package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
- package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
- package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
- package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
- package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
- package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
- package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
- package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
- package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
- package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
- package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
- package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
- package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
- package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
- package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
- package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
- package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
- package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
- package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
- package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
- package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
- package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
- package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
- package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
- package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
- package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
- package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
- package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
- package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
- package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
- package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
- package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
- package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
- package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
- package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
- package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
- package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
- package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
- package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
- package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
- package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
- package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
- package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
- package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
- package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
- package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
- package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
- package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
- package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
- package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
- package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
- package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
- package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
- package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
- package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
- package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
- package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
- package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
- package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
- package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
- package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
- package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
- package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
- package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
- package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
- package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
- package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
- package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
- package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
- package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
- package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
- package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
- package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
- package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
- package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
- package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
- package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
- package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
- package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
- package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
- package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
- package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
- package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
- package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
- package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
- package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
- package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
- package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
- package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
- package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
- package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
- package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
- package/web/fonts/LICENSE.txt +287 -0
- package/web/icon.svg +24 -0
- package/web/index.html +19 -0
- package/web/manifest.webmanifest +18 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { app } from './state.svelte';
|
|
2
|
+
import {
|
|
3
|
+
cancelScheduledAlarm,
|
|
4
|
+
claimScheduledAlarm,
|
|
5
|
+
playAlarmOnce,
|
|
6
|
+
primeAudio,
|
|
7
|
+
scheduleAlarm,
|
|
8
|
+
setTabAlert,
|
|
9
|
+
startLoopingAlarm,
|
|
10
|
+
stopAlarmSound,
|
|
11
|
+
} from './timer-alarm.svelte';
|
|
12
|
+
import type { PomodoroSettings } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tomato timer that lives entirely in the controller's browser.
|
|
16
|
+
*
|
|
17
|
+
* The DSH panel only persists settings; this module owns the countdown. Timing
|
|
18
|
+
* always derives from an absolute `endsAt` timestamp, so moving between console
|
|
19
|
+
* pages (or a throttled background tab) never loses time. The runtime is a
|
|
20
|
+
* singleton because the console renders an infinite-loop clone of every page.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type PomodoroPhase = 'focus' | 'break';
|
|
24
|
+
|
|
25
|
+
export interface PomodoroRuntime {
|
|
26
|
+
phase: PomodoroPhase;
|
|
27
|
+
running: boolean;
|
|
28
|
+
remainingMs: number;
|
|
29
|
+
endsAt: number;
|
|
30
|
+
finished: boolean;
|
|
31
|
+
rounds: number;
|
|
32
|
+
/** True while the end-of-timer alarm is still ringing (loop mode). */
|
|
33
|
+
alarmActive: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const pomodoro = $state<PomodoroRuntime>({
|
|
37
|
+
phase: 'focus',
|
|
38
|
+
running: false,
|
|
39
|
+
remainingMs: 25 * 60_000,
|
|
40
|
+
endsAt: 0,
|
|
41
|
+
finished: false,
|
|
42
|
+
rounds: 0,
|
|
43
|
+
alarmActive: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let refs = 0;
|
|
47
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
48
|
+
let durationSignature = '';
|
|
49
|
+
|
|
50
|
+
function settings(): PomodoroSettings {
|
|
51
|
+
return app.pomodoro;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function phaseDurationMs(phase: PomodoroPhase = pomodoro.phase, value: PomodoroSettings = settings()): number {
|
|
55
|
+
const minutes = phase === 'focus' ? value.focusMinutes : value.breakMinutes;
|
|
56
|
+
const limit = phase === 'focus' ? 180 : 120;
|
|
57
|
+
const fallback = phase === 'focus' ? 25 : 5;
|
|
58
|
+
const raw = Number(minutes);
|
|
59
|
+
const safe = Number.isFinite(raw) ? Math.max(0.1, Math.min(limit, raw)) : fallback;
|
|
60
|
+
// Fractional minutes are allowed in settings; the timer works in whole seconds.
|
|
61
|
+
return Math.max(1, Math.round(safe * 60)) * 1000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatRemaining(ms: number): string {
|
|
65
|
+
const total = Math.max(0, Math.ceil(ms / 1000));
|
|
66
|
+
const minutes = Math.floor(total / 60);
|
|
67
|
+
const seconds = total % 60;
|
|
68
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function pomodoroProgress(): number {
|
|
72
|
+
const duration = phaseDurationMs();
|
|
73
|
+
if (duration <= 0) return 0;
|
|
74
|
+
return Math.max(0, Math.min(100, ((duration - pomodoro.remainingMs) / duration) * 100));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function syncDuration(): void {
|
|
78
|
+
const signature = `${settings().focusMinutes}/${settings().breakMinutes}`;
|
|
79
|
+
if (signature === durationSignature) return;
|
|
80
|
+
durationSignature = signature;
|
|
81
|
+
if (!pomodoro.running && !pomodoro.finished) pomodoro.remainingMs = phaseDurationMs();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resetRuntime(): void {
|
|
85
|
+
pomodoro.phase = 'focus';
|
|
86
|
+
pomodoro.running = false;
|
|
87
|
+
pomodoro.remainingMs = phaseDurationMs('focus');
|
|
88
|
+
pomodoro.endsAt = 0;
|
|
89
|
+
pomodoro.finished = false;
|
|
90
|
+
pomodoro.rounds = 0;
|
|
91
|
+
stopAlarm();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function startEngine(): void {
|
|
95
|
+
durationSignature = `${settings().focusMinutes}/${settings().breakMinutes}`;
|
|
96
|
+
if (!pomodoro.running && !pomodoro.finished) pomodoro.remainingMs = phaseDurationMs();
|
|
97
|
+
if (timer) return;
|
|
98
|
+
timer = setInterval(tick, 250);
|
|
99
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
100
|
+
(window as unknown as { __DSH_POMODORO__?: PomodoroRuntime }).__DSH_POMODORO__ = pomodoro;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stopEngine(): void {
|
|
104
|
+
if (timer) clearInterval(timer);
|
|
105
|
+
timer = null;
|
|
106
|
+
document.removeEventListener('visibilitychange', handleVisibility);
|
|
107
|
+
cancelScheduledAlarm();
|
|
108
|
+
resetRuntime();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleVisibility(): void {
|
|
112
|
+
if (!document.hidden) tick();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function tick(): void {
|
|
116
|
+
if (!settings().enabled) {
|
|
117
|
+
resetRuntime();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
syncDuration();
|
|
121
|
+
if (pomodoro.alarmActive && !settings().loopAlarm) stopAlarm();
|
|
122
|
+
if (!pomodoro.running) return;
|
|
123
|
+
const remaining = pomodoro.endsAt - Date.now();
|
|
124
|
+
if (remaining <= 0) {
|
|
125
|
+
pomodoro.remainingMs = 0;
|
|
126
|
+
finish();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
pomodoro.remainingMs = remaining;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Reference-counted lifecycle; the card (and its clone) both acquire it. */
|
|
133
|
+
export function acquirePomodoro(): () => void {
|
|
134
|
+
refs += 1;
|
|
135
|
+
if (refs === 1) startEngine();
|
|
136
|
+
let released = false;
|
|
137
|
+
return () => {
|
|
138
|
+
if (released) return;
|
|
139
|
+
released = true;
|
|
140
|
+
refs = Math.max(0, refs - 1);
|
|
141
|
+
if (refs === 0) stopEngine();
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function startPomodoro(): void {
|
|
146
|
+
if (!settings().enabled) return;
|
|
147
|
+
primeAudio();
|
|
148
|
+
if (pomodoro.finished) {
|
|
149
|
+
pomodoro.phase = pomodoro.phase === 'focus' ? 'break' : 'focus';
|
|
150
|
+
pomodoro.finished = false;
|
|
151
|
+
pomodoro.remainingMs = phaseDurationMs();
|
|
152
|
+
}
|
|
153
|
+
if (pomodoro.remainingMs <= 0) pomodoro.remainingMs = phaseDurationMs();
|
|
154
|
+
pomodoro.running = true;
|
|
155
|
+
pomodoro.endsAt = Date.now() + pomodoro.remainingMs;
|
|
156
|
+
stopAlarm();
|
|
157
|
+
if (!settings().soundPath.trim()) scheduleAlarm(Math.max(0.06, (pomodoro.endsAt - Date.now()) / 1000), pomodoro.endsAt);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function pausePomodoro(): void {
|
|
161
|
+
if (!pomodoro.running) return;
|
|
162
|
+
pomodoro.remainingMs = Math.max(0, pomodoro.endsAt - Date.now());
|
|
163
|
+
pomodoro.running = false;
|
|
164
|
+
pomodoro.endsAt = 0;
|
|
165
|
+
cancelScheduledAlarm();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function togglePomodoro(): void {
|
|
169
|
+
if (pomodoro.running) pausePomodoro();
|
|
170
|
+
else startPomodoro();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resetPomodoro(): void {
|
|
174
|
+
pomodoro.running = false;
|
|
175
|
+
pomodoro.finished = false;
|
|
176
|
+
pomodoro.phase = 'focus';
|
|
177
|
+
pomodoro.rounds = 0;
|
|
178
|
+
pomodoro.endsAt = 0;
|
|
179
|
+
pomodoro.remainingMs = phaseDurationMs('focus');
|
|
180
|
+
stopAlarm();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Switch focus/break while idle (the label in the card is a button). */
|
|
184
|
+
export function switchPomodoroPhase(): void {
|
|
185
|
+
if (pomodoro.running) return;
|
|
186
|
+
pomodoro.phase = pomodoro.phase === 'focus' ? 'break' : 'focus';
|
|
187
|
+
pomodoro.finished = false;
|
|
188
|
+
pomodoro.remainingMs = phaseDurationMs();
|
|
189
|
+
stopAlarm();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Acknowledge the end-of-timer alarm and stop the looping sound. */
|
|
193
|
+
export function dismissAlarm(): void {
|
|
194
|
+
stopAlarm();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function finish(): void {
|
|
198
|
+
const finishedPhase = pomodoro.phase;
|
|
199
|
+
pomodoro.running = false;
|
|
200
|
+
pomodoro.remainingMs = 0;
|
|
201
|
+
pomodoro.finished = true;
|
|
202
|
+
pomodoro.alarmActive = false;
|
|
203
|
+
if (finishedPhase === 'focus') pomodoro.rounds += 1;
|
|
204
|
+
const hasScheduledChime = claimScheduledAlarm(pomodoro.endsAt);
|
|
205
|
+
if (settings().loopAlarm) {
|
|
206
|
+
pomodoro.alarmActive = true;
|
|
207
|
+
startLoopingAlarm(settings().soundPath, hasScheduledChime);
|
|
208
|
+
} else if (!hasScheduledChime) {
|
|
209
|
+
playAlarmOnce(settings().soundPath);
|
|
210
|
+
}
|
|
211
|
+
setTabAlert(true);
|
|
212
|
+
try {
|
|
213
|
+
navigator.vibrate?.([140, 90, 180]);
|
|
214
|
+
} catch {
|
|
215
|
+
/* vibration is optional */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Stop the alert sound and clear the runtime flag. */
|
|
220
|
+
function stopAlarm(): void {
|
|
221
|
+
stopAlarmSound();
|
|
222
|
+
pomodoro.alarmActive = false;
|
|
223
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SessionItem } from './types';
|
|
2
|
+
|
|
3
|
+
/** One session row plus the subagents that session dispatched. */
|
|
4
|
+
export interface SessionNode {
|
|
5
|
+
session: SessionItem;
|
|
6
|
+
children: SessionNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const STATUS_RANK: Record<string, number> = { question: 0, running: 1, error: 2, done: 3 };
|
|
10
|
+
|
|
11
|
+
function statusRank(session: SessionItem): number {
|
|
12
|
+
return STATUS_RANK[session.status ?? 'done'] ?? 3;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Group subagent sessions under their durable parent session.
|
|
17
|
+
*
|
|
18
|
+
* A subagent that is active keeps its parent group near the top: a node's rank
|
|
19
|
+
* is the best rank in its whole subtree, and ties fall back to the newest
|
|
20
|
+
* activity in that subtree. Sessions whose parent is not in the live list stay
|
|
21
|
+
* at the top level (still marked as subagents), and malformed lineage falls
|
|
22
|
+
* back to a root instead of looping.
|
|
23
|
+
*/
|
|
24
|
+
export function buildSessionTree(sessions: SessionItem[]): SessionNode[] {
|
|
25
|
+
const nodes = new Map<string, SessionNode>();
|
|
26
|
+
const ids: string[] = [];
|
|
27
|
+
sessions.forEach((session, index) => {
|
|
28
|
+
const id = session.id || `index-${index}`;
|
|
29
|
+
ids.push(id);
|
|
30
|
+
nodes.set(id, { session, children: [] });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const parentOf = new Map<string, string>();
|
|
34
|
+
sessions.forEach((session, index) => {
|
|
35
|
+
const parentId = session.parentId || '';
|
|
36
|
+
if (parentId) parentOf.set(ids[index], parentId);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function createsCycle(id: string, parentId: string): boolean {
|
|
40
|
+
let current = parentId;
|
|
41
|
+
for (let guard = 0; current && guard < ids.length + 1; guard += 1) {
|
|
42
|
+
if (current === id) return true;
|
|
43
|
+
current = parentOf.get(current) || '';
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const roots: SessionNode[] = [];
|
|
49
|
+
sessions.forEach((session, index) => {
|
|
50
|
+
const id = ids[index];
|
|
51
|
+
const node = nodes.get(id)!;
|
|
52
|
+
const parentId = session.parentId || '';
|
|
53
|
+
const parent = parentId ? nodes.get(parentId) : undefined;
|
|
54
|
+
if (parent && parentId !== id && !createsCycle(id, parentId)) parent.children.push(node);
|
|
55
|
+
else roots.push(node);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function subtreeRank(node: SessionNode): number {
|
|
59
|
+
let rank = statusRank(node.session);
|
|
60
|
+
for (const child of node.children) rank = Math.min(rank, subtreeRank(child));
|
|
61
|
+
return rank;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function subtreeActivity(node: SessionNode): number {
|
|
65
|
+
let latest = Number(node.session.lastActiveAt) || 0;
|
|
66
|
+
for (const child of node.children) latest = Math.max(latest, subtreeActivity(child));
|
|
67
|
+
return latest;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sortNodes(list: SessionNode[]): void {
|
|
71
|
+
list.sort((a, b) => {
|
|
72
|
+
const byRank = subtreeRank(a) - subtreeRank(b);
|
|
73
|
+
return byRank !== 0 ? byRank : subtreeActivity(b) - subtreeActivity(a);
|
|
74
|
+
});
|
|
75
|
+
for (const node of list) sortNodes(node.children);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sortNodes(roots);
|
|
79
|
+
return roots;
|
|
80
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { setLocale, t } from './i18n.svelte';
|
|
2
|
+
import { DEFAULT_ORDER, enabledOrder, normalizeOrder } from './pages';
|
|
3
|
+
import { configureLive, connectLive, disposeLive, isLiveOpen, sendLive } from './ws';
|
|
4
|
+
import type {
|
|
5
|
+
AccountData,
|
|
6
|
+
ConsoleSettings,
|
|
7
|
+
ConsoleState,
|
|
8
|
+
CountdownSettings,
|
|
9
|
+
MediaPayload,
|
|
10
|
+
NavPosition,
|
|
11
|
+
NotificationsData,
|
|
12
|
+
OverviewData,
|
|
13
|
+
PerformanceData,
|
|
14
|
+
PomodoroSettings,
|
|
15
|
+
QuestionAnswerItem,
|
|
16
|
+
ServerMessage,
|
|
17
|
+
SessionItem,
|
|
18
|
+
WeatherData,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const NAV_POSITIONS: readonly NavPosition[] = ['left', 'right', 'top', 'bottom'];
|
|
22
|
+
|
|
23
|
+
export const COUNTDOWN_PRESET_LIMIT = 5;
|
|
24
|
+
export const COUNTDOWN_MAX_SECONDS = 86_400;
|
|
25
|
+
/** Presets a fresh profile starts with; the user may keep one to five. */
|
|
26
|
+
export const DEFAULT_COUNTDOWN_PRESETS = [60, 300, 600];
|
|
27
|
+
|
|
28
|
+
export function normalizeNavPosition(value: unknown): NavPosition {
|
|
29
|
+
return NAV_POSITIONS.includes(value as NavPosition) ? (value as NavPosition) : 'left';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizePomodoro(input: Partial<PomodoroSettings> | undefined, base: PomodoroSettings): PomodoroSettings {
|
|
33
|
+
const minutes = (value: unknown, fallback: number, max: number): number => {
|
|
34
|
+
const number = Number(value);
|
|
35
|
+
return Number.isFinite(number) ? Math.max(0.1, Math.min(max, Math.round(number * 100) / 100)) : fallback;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
enabled: input?.enabled === true,
|
|
39
|
+
focusMinutes: minutes(input?.focusMinutes, base.focusMinutes, 180),
|
|
40
|
+
breakMinutes: minutes(input?.breakMinutes, base.breakMinutes, 120),
|
|
41
|
+
soundPath: typeof input?.soundPath === 'string' ? input.soundPath : base.soundPath,
|
|
42
|
+
loopAlarm: input?.loopAlarm === true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCountdown(
|
|
47
|
+
input: Partial<CountdownSettings> | undefined,
|
|
48
|
+
base: CountdownSettings,
|
|
49
|
+
): CountdownSettings {
|
|
50
|
+
const source = Array.isArray(input?.presets) ? input.presets : [];
|
|
51
|
+
const fallbackPresets = base.presets.length > 0 ? base.presets : DEFAULT_COUNTDOWN_PRESETS;
|
|
52
|
+
const presets: number[] = [];
|
|
53
|
+
for (const entry of source.slice(0, COUNTDOWN_PRESET_LIMIT)) {
|
|
54
|
+
const seconds = Math.round(Number(entry));
|
|
55
|
+
if (!Number.isFinite(seconds)) continue;
|
|
56
|
+
presets.push(Math.max(1, Math.min(COUNTDOWN_MAX_SECONDS, seconds)));
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
enabled: input?.enabled === true,
|
|
60
|
+
presets: presets.length > 0
|
|
61
|
+
? presets
|
|
62
|
+
: fallbackPresets.slice(0, COUNTDOWN_PRESET_LIMIT),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Shared live state for the mobile console.
|
|
68
|
+
*
|
|
69
|
+
* Svelte 5 runes make this plain object deeply reactive, so every page (and its
|
|
70
|
+
* infinite-loop clone) renders from the same WebSocket snapshot without the
|
|
71
|
+
* old imperative `syncClones()` DOM copying.
|
|
72
|
+
*/
|
|
73
|
+
export const app: ConsoleState = $state({
|
|
74
|
+
online: null,
|
|
75
|
+
theme: 'dark',
|
|
76
|
+
temperatureUnit: 'celsius',
|
|
77
|
+
pushQuestions: false,
|
|
78
|
+
navPosition: 'left',
|
|
79
|
+
pomodoro: { enabled: false, focusMinutes: 25, breakMinutes: 5, soundPath: '', loopAlarm: false },
|
|
80
|
+
countdown: { enabled: false, presets: [...DEFAULT_COUNTDOWN_PRESETS] },
|
|
81
|
+
balanceDisplay: { showPeakValley: true, spendWindow: 'calendar' },
|
|
82
|
+
pageOrder: [...DEFAULT_ORDER],
|
|
83
|
+
pageEnabled: { home: true, sessions: true, music: true, performance: true },
|
|
84
|
+
pageRevision: 0,
|
|
85
|
+
|
|
86
|
+
overview: null,
|
|
87
|
+
weather: null,
|
|
88
|
+
notifications: null,
|
|
89
|
+
account: null,
|
|
90
|
+
sessions: [],
|
|
91
|
+
sessionsError: null,
|
|
92
|
+
performance: null,
|
|
93
|
+
|
|
94
|
+
media: null,
|
|
95
|
+
clockNow: Date.now(),
|
|
96
|
+
|
|
97
|
+
volumeDragging: false,
|
|
98
|
+
volumeInput: 0.2,
|
|
99
|
+
volumeTarget: 0.2,
|
|
100
|
+
volumeHoldUntil: 0,
|
|
101
|
+
|
|
102
|
+
toastVisible: false,
|
|
103
|
+
toastMessage: '',
|
|
104
|
+
|
|
105
|
+
activeKey: 'home',
|
|
106
|
+
initialKey: null,
|
|
107
|
+
hasNavigated: false,
|
|
108
|
+
fullscreen: false,
|
|
109
|
+
answerSessionId: null,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
113
|
+
|
|
114
|
+
export function setOnline(value: boolean | null): void {
|
|
115
|
+
app.online = value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function toast(message: string): void {
|
|
119
|
+
app.toastMessage = String(message || '');
|
|
120
|
+
app.toastVisible = true;
|
|
121
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
122
|
+
toastTimer = setTimeout(() => {
|
|
123
|
+
app.toastVisible = false;
|
|
124
|
+
}, 2600);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function sendAction(action: string, value?: number | boolean): boolean {
|
|
128
|
+
const message = { type: 'action' as const, action, ...(value !== undefined ? { value } : {}) };
|
|
129
|
+
if (!sendLive(message)) {
|
|
130
|
+
toast(t('connecting'));
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface AnswerWaiter {
|
|
137
|
+
resolve: () => void;
|
|
138
|
+
reject: (error: Error) => void;
|
|
139
|
+
timer: ReturnType<typeof setTimeout>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const answerWaiters = new Map<string, AnswerWaiter>();
|
|
143
|
+
|
|
144
|
+
function answerKey(sessionId: string, requestId: string): string {
|
|
145
|
+
return `${sessionId}\u0000${requestId}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Open the quick-reply sheet for a session waiting on a question. */
|
|
149
|
+
export function openQuestion(sessionId: string): void {
|
|
150
|
+
const session = app.sessions.find((item) => item.id === sessionId);
|
|
151
|
+
if (!session) return;
|
|
152
|
+
if (!app.pushQuestions) {
|
|
153
|
+
toast(t('pushQuestionsOff'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (session.pending?.kind === 'approval') {
|
|
157
|
+
toast(t('approvalPending'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (session.pending?.answerable === false) {
|
|
161
|
+
toast(t('answerInDsh'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!session.pending?.questions?.length) {
|
|
165
|
+
toast(t('answerInDsh'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
app.answerSessionId = sessionId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Close the quick-reply sheet without answering. */
|
|
172
|
+
export function closeQuestion(): void {
|
|
173
|
+
app.answerSessionId = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Submit one complete answer batch and wait for the Host acknowledgement. */
|
|
177
|
+
export function submitQuestionAnswer(
|
|
178
|
+
sessionId: string,
|
|
179
|
+
requestId: string,
|
|
180
|
+
answers: QuestionAnswerItem[],
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
if (!isLiveOpen()) {
|
|
184
|
+
reject(new Error(t('connectionLost')));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const key = answerKey(sessionId, requestId);
|
|
188
|
+
const previous = answerWaiters.get(key);
|
|
189
|
+
if (previous) {
|
|
190
|
+
clearTimeout(previous.timer);
|
|
191
|
+
answerWaiters.delete(key);
|
|
192
|
+
previous.reject(new Error(t('answerReplaced')));
|
|
193
|
+
}
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
const waiter = answerWaiters.get(key);
|
|
196
|
+
if (!waiter) return;
|
|
197
|
+
answerWaiters.delete(key);
|
|
198
|
+
waiter.reject(new Error(t('answerTimeout')));
|
|
199
|
+
}, 12_000);
|
|
200
|
+
answerWaiters.set(key, { resolve, reject, timer });
|
|
201
|
+
if (!sendLive({ type: 'answer-question', sessionId, requestId, answers })) {
|
|
202
|
+
answerWaiters.delete(key);
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
reject(new Error(t('connectionLost')));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function applyTheme(theme: string): void {
|
|
210
|
+
app.theme = theme === 'light' ? 'light' : 'dark';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function applySettings(settings: ConsoleSettings | undefined): void {
|
|
214
|
+
if (!settings) return;
|
|
215
|
+
if (settings.theme) applyTheme(settings.theme);
|
|
216
|
+
setLocale(settings.locale);
|
|
217
|
+
if (settings.temperatureUnit) {
|
|
218
|
+
app.temperatureUnit = settings.temperatureUnit === 'fahrenheit' ? 'fahrenheit' : 'celsius';
|
|
219
|
+
}
|
|
220
|
+
app.pushQuestions = settings.pushQuestions === true;
|
|
221
|
+
if (settings.navPosition) app.navPosition = normalizeNavPosition(settings.navPosition);
|
|
222
|
+
if (settings.pomodoro) app.pomodoro = normalizePomodoro(settings.pomodoro, app.pomodoro);
|
|
223
|
+
if (settings.countdown) app.countdown = normalizeCountdown(settings.countdown, app.countdown);
|
|
224
|
+
// The Host enforces this too; mirror it so a stale or hand-built snapshot can
|
|
225
|
+
// never render both timer cards at once.
|
|
226
|
+
if (app.pomodoro.enabled) app.countdown.enabled = false;
|
|
227
|
+
else if (app.countdown.enabled) app.pomodoro.enabled = false;
|
|
228
|
+
if (settings.balance) {
|
|
229
|
+
app.balanceDisplay = {
|
|
230
|
+
showPeakValley: settings.balance.showPeakValley !== false,
|
|
231
|
+
spendWindow: settings.balance.spendWindow === 'rolling' ? 'rolling' : 'calendar',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
let pageChanged = false;
|
|
235
|
+
if (settings.pageOrder) {
|
|
236
|
+
app.pageOrder = normalizeOrder(settings.pageOrder);
|
|
237
|
+
pageChanged = true;
|
|
238
|
+
}
|
|
239
|
+
if (settings.pageEnabled) {
|
|
240
|
+
app.pageEnabled = { ...app.pageEnabled, ...settings.pageEnabled };
|
|
241
|
+
pageChanged = true;
|
|
242
|
+
}
|
|
243
|
+
if (pageChanged) app.pageRevision += 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function applyOverview(data: OverviewData | undefined): void {
|
|
247
|
+
if (!data) return;
|
|
248
|
+
app.overview = data;
|
|
249
|
+
applySettings(data.settings);
|
|
250
|
+
app.weather = data.weather ?? null;
|
|
251
|
+
app.notifications = data.notifications ?? null;
|
|
252
|
+
app.account = data.account ?? null;
|
|
253
|
+
app.sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
254
|
+
app.sessionsError = data.sessionsError || null;
|
|
255
|
+
if (data.performance) app.performance = data.performance;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function applyMedia(payload: MediaPayload | undefined): void {
|
|
259
|
+
app.media = payload ?? null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleMessage(message: ServerMessage): void {
|
|
263
|
+
switch (message?.type) {
|
|
264
|
+
case 'welcome':
|
|
265
|
+
setOnline(true);
|
|
266
|
+
applySettings(message.settings);
|
|
267
|
+
break;
|
|
268
|
+
case 'overview':
|
|
269
|
+
setOnline(true);
|
|
270
|
+
applyOverview(message.data as OverviewData | undefined);
|
|
271
|
+
break;
|
|
272
|
+
case 'weather':
|
|
273
|
+
app.weather = (message.data as WeatherData | undefined) ?? null;
|
|
274
|
+
break;
|
|
275
|
+
case 'notifications':
|
|
276
|
+
app.notifications = (message.data as NotificationsData | undefined) ?? null;
|
|
277
|
+
break;
|
|
278
|
+
case 'sessions':
|
|
279
|
+
app.sessions = Array.isArray(message.data) ? (message.data as SessionItem[]) : [];
|
|
280
|
+
app.sessionsError = null;
|
|
281
|
+
break;
|
|
282
|
+
case 'account':
|
|
283
|
+
app.account = (message.data as AccountData | undefined) ?? null;
|
|
284
|
+
break;
|
|
285
|
+
case 'media':
|
|
286
|
+
setOnline(true);
|
|
287
|
+
applyMedia(message.data as MediaPayload | undefined);
|
|
288
|
+
break;
|
|
289
|
+
case 'performance':
|
|
290
|
+
app.performance = (message.data as PerformanceData | undefined) ?? null;
|
|
291
|
+
break;
|
|
292
|
+
case 'settings':
|
|
293
|
+
applySettings(message.data as ConsoleSettings | undefined);
|
|
294
|
+
break;
|
|
295
|
+
case 'action-result':
|
|
296
|
+
if (!message.ok) toast(message.error || t('actionFailed'));
|
|
297
|
+
break;
|
|
298
|
+
case 'answer-result': {
|
|
299
|
+
const key = answerKey(message.sessionId || '', message.requestId || '');
|
|
300
|
+
const waiter = answerWaiters.get(key);
|
|
301
|
+
if (!waiter) break;
|
|
302
|
+
answerWaiters.delete(key);
|
|
303
|
+
clearTimeout(waiter.timer);
|
|
304
|
+
if (message.ok) waiter.resolve();
|
|
305
|
+
else waiter.reject(new Error(message.error || t('answerFailed')));
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case 'error':
|
|
309
|
+
toast(message.error || t('serviceError'));
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function startLive(): void {
|
|
317
|
+
configureLive({
|
|
318
|
+
onOpen() {
|
|
319
|
+
setOnline(true);
|
|
320
|
+
sendLive({ type: 'settings' });
|
|
321
|
+
},
|
|
322
|
+
onMessage: handleMessage,
|
|
323
|
+
onClose() {
|
|
324
|
+
setOnline(false);
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
connectLive();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function stopLive(): void {
|
|
331
|
+
disposeLive();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** One shared second tick for the clock; media progress is owned by SeekBar. */
|
|
335
|
+
export function startTicks(): () => void {
|
|
336
|
+
app.clockNow = Date.now();
|
|
337
|
+
const clockTimer = setInterval(() => {
|
|
338
|
+
app.clockNow = Date.now();
|
|
339
|
+
}, 1000);
|
|
340
|
+
return () => {
|
|
341
|
+
clearInterval(clockTimer);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function restoreUrl(): void {
|
|
346
|
+
if (location.search.includes('token=') || location.search.includes('page=')) {
|
|
347
|
+
try {
|
|
348
|
+
history.replaceState({}, '', location.pathname);
|
|
349
|
+
} catch {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function exposeState(): void {
|
|
356
|
+
window.__DSH_REMOTE__ = app;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export { enabledOrder, normalizeOrder };
|