@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser entry for the dsh-remote-control sidebar page.
|
|
3
|
+
*
|
|
4
|
+
* `scripts/build-dsh.mjs` compiles the modules under this directory and wraps
|
|
5
|
+
* them in the DSH module-loader factory; the package still exposes the result
|
|
6
|
+
* as a single `client.js`. This file only wires the DSH lifecycle: locale
|
|
7
|
+
* registration, theme synchronization, the sidebar launcher, and the main
|
|
8
|
+
* panel registration.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { LOCALE_URL, THEME_URL } from './api.js';
|
|
12
|
+
import { DICTS, NS, configureLocale, en, translate, zh } from './i18n.js';
|
|
13
|
+
import { PanelIcon } from './icons.js';
|
|
14
|
+
import { RemotePanel } from './panel.js';
|
|
15
|
+
import type { ClientContext, ThemeSnapshot } from './types.js';
|
|
16
|
+
|
|
17
|
+
const PANEL_ID = 'remote-control';
|
|
18
|
+
|
|
19
|
+
export const inject: string[] = ['slots', 'locale', 'theme'];
|
|
20
|
+
|
|
21
|
+
export function apply(ctx: ClientContext): void {
|
|
22
|
+
configureLocale(ctx.locale);
|
|
23
|
+
ctx.effect(() => ctx.locale.register(NS, DICTS), 'dsh-remote-control: dictionaries');
|
|
24
|
+
ctx.effect(() => {
|
|
25
|
+
/**
|
|
26
|
+
* Tell the Host which language DSH actually renders. The phone console has
|
|
27
|
+
* no access to the DSH locale service, so "follow DSH" can only be resolved
|
|
28
|
+
* once this snapshot reaches the Host.
|
|
29
|
+
*/
|
|
30
|
+
const sync = (): void => {
|
|
31
|
+
try {
|
|
32
|
+
const snapshot = ctx.locale.getLocale?.() ?? ctx.locale.getSnapshot?.();
|
|
33
|
+
const active = String(snapshot?.active || '');
|
|
34
|
+
if (!active) return;
|
|
35
|
+
void fetch(LOCALE_URL, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
credentials: 'same-origin',
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ locale: active }),
|
|
40
|
+
}).catch(() => {});
|
|
41
|
+
} catch {
|
|
42
|
+
/* a locale service without snapshots still renders the bundled default */
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
sync();
|
|
46
|
+
let off: (() => void) | undefined;
|
|
47
|
+
try {
|
|
48
|
+
off = ctx.locale.subscribe?.(sync);
|
|
49
|
+
} catch {
|
|
50
|
+
off = undefined;
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
try {
|
|
54
|
+
off?.();
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore a disposed locale service */
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}, 'dsh-remote-control: locale sync');
|
|
60
|
+
ctx.effect(() => {
|
|
61
|
+
let active = true;
|
|
62
|
+
const sync = (snapshot?: ThemeSnapshot): void => {
|
|
63
|
+
if (!active) return;
|
|
64
|
+
const scheme = snapshot?.active?.colorScheme || ctx.theme?.getTheme?.()?.active?.colorScheme;
|
|
65
|
+
if (!scheme) return;
|
|
66
|
+
void fetch(THEME_URL, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
credentials: 'same-origin',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ theme: scheme }),
|
|
71
|
+
}).catch(() => {});
|
|
72
|
+
};
|
|
73
|
+
sync(ctx.theme?.getTheme?.());
|
|
74
|
+
const off = ctx.on?.('theme/change', sync);
|
|
75
|
+
return () => {
|
|
76
|
+
active = false;
|
|
77
|
+
try { off?.(); } catch { /* ignore */ }
|
|
78
|
+
};
|
|
79
|
+
}, 'dsh-remote-control: theme sync');
|
|
80
|
+
ctx.effect(
|
|
81
|
+
() => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
82
|
+
name: 'sidebar.panellist',
|
|
83
|
+
id: PANEL_ID,
|
|
84
|
+
order: 12,
|
|
85
|
+
label: () => translate('title'),
|
|
86
|
+
}, PanelIcon)),
|
|
87
|
+
'dsh-remote-control: sidebar launcher',
|
|
88
|
+
);
|
|
89
|
+
ctx.effect(
|
|
90
|
+
() => ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: PANEL_ID }, RemotePanel)),
|
|
91
|
+
'dsh-remote-control: settings panel',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const __test = { zh, en, PanelIcon, RemotePanel };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/** Pure state helpers shared by the panel and its section renderers. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CountdownPresetDraft,
|
|
5
|
+
DraftState,
|
|
6
|
+
LocationResult,
|
|
7
|
+
RemoteApp,
|
|
8
|
+
RemoteSettings,
|
|
9
|
+
SavePatch,
|
|
10
|
+
SettingsPayload,
|
|
11
|
+
StagedSettings,
|
|
12
|
+
Translator,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import { isRecord } from './api.js';
|
|
15
|
+
|
|
16
|
+
export const PAGE_ORDER: string[] = ['home', 'sessions', 'music', 'performance'];
|
|
17
|
+
|
|
18
|
+
/** Upper bound for user-configured countdown presets. */
|
|
19
|
+
export const COUNTDOWN_PRESET_LIMIT = 5;
|
|
20
|
+
/** Smallest and largest accepted countdown duration, in seconds. */
|
|
21
|
+
export const COUNTDOWN_MIN_SECONDS = 1;
|
|
22
|
+
export const COUNTDOWN_MAX_SECONDS = 86_400;
|
|
23
|
+
/** Presets a fresh profile starts with; the user may keep one to five. */
|
|
24
|
+
export const DEFAULT_COUNTDOWN_PRESETS: number[] = [60, 300, 600];
|
|
25
|
+
|
|
26
|
+
/** Pomodoro duration bounds, mirrored from the Host normalizer for the UI. */
|
|
27
|
+
export const POMODORO_MIN_MINUTES = 0.1;
|
|
28
|
+
export const POMODORO_MAX_FOCUS_MINUTES = 180;
|
|
29
|
+
export const POMODORO_MAX_BREAK_MINUTES = 120;
|
|
30
|
+
|
|
31
|
+
export const EMPTY_DRAFT: DraftState = {
|
|
32
|
+
certPath: '',
|
|
33
|
+
keyPath: '',
|
|
34
|
+
locationName: '',
|
|
35
|
+
latitude: '',
|
|
36
|
+
longitude: '',
|
|
37
|
+
threshold: '',
|
|
38
|
+
notificationLimit: '5',
|
|
39
|
+
pomodoroFocus: '25',
|
|
40
|
+
pomodoroBreak: '5',
|
|
41
|
+
pomodoroSound: '',
|
|
42
|
+
countdownPresets: createCountdownDraft(DEFAULT_COUNTDOWN_PRESETS),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Parse one countdown preset input; null means the slot is not configured. */
|
|
46
|
+
export function parseCountdownPreset(value: unknown, fallback: number | null = null): number | null {
|
|
47
|
+
const raw = typeof value === 'string' ? value.trim() : value;
|
|
48
|
+
if (raw === '' || raw === null || raw === undefined) return fallback;
|
|
49
|
+
const seconds = Math.round(Number(raw));
|
|
50
|
+
if (!Number.isFinite(seconds)) return fallback;
|
|
51
|
+
return Math.max(COUNTDOWN_MIN_SECONDS, Math.min(COUNTDOWN_MAX_SECONDS, seconds));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPresetDraft(value: unknown): value is CountdownPresetDraft {
|
|
55
|
+
return isRecord(value) && typeof value.minutes === 'string' && typeof value.seconds === 'string';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parse one minute/second preset row; null means the row is empty. */
|
|
59
|
+
export function presetDraftToSeconds(preset: CountdownPresetDraft): number | null {
|
|
60
|
+
const minutesText = preset.minutes.trim();
|
|
61
|
+
const secondsText = preset.seconds.trim();
|
|
62
|
+
if (minutesText === '' && secondsText === '') return null;
|
|
63
|
+
const minutes = Number(minutesText || 0);
|
|
64
|
+
const seconds = Number(secondsText || 0);
|
|
65
|
+
if (!Number.isFinite(minutes) || !Number.isFinite(seconds)) return null;
|
|
66
|
+
const total = Math.round(minutes * 60 + seconds);
|
|
67
|
+
return Math.max(COUNTDOWN_MIN_SECONDS, Math.min(COUNTDOWN_MAX_SECONDS, total));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Keep a minute field to digits with at most two decimal places. The raw text
|
|
72
|
+
* is preserved otherwise, so "2", "2.5" and "2.55" all keep their own shape.
|
|
73
|
+
*/
|
|
74
|
+
export function sanitizeMinuteInput(value: string): string {
|
|
75
|
+
const cleaned = value.replace(/[^\d.]/g, '');
|
|
76
|
+
const dot = cleaned.indexOf('.');
|
|
77
|
+
if (dot < 0) return cleaned;
|
|
78
|
+
const whole = cleaned.slice(0, dot);
|
|
79
|
+
const fraction = cleaned.slice(dot + 1).replace(/\./g, '').slice(0, 2);
|
|
80
|
+
return `${whole}.${fraction}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clamp a minute field on blur. In-range text stays exactly as typed; anything
|
|
85
|
+
* outside the bounds (or unparsable) is capped to the nearest limit.
|
|
86
|
+
*/
|
|
87
|
+
export function capMinuteInput(value: string, min: number, max: number): string {
|
|
88
|
+
const text = value.trim();
|
|
89
|
+
if (text === '') return '';
|
|
90
|
+
const minutes = Number(text);
|
|
91
|
+
if (!Number.isFinite(minutes)) return '';
|
|
92
|
+
if (minutes >= min && minutes <= max) return value;
|
|
93
|
+
return String(Math.max(min, Math.min(max, minutes)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clamp one countdown preset row on blur: total seconds are capped to the
|
|
98
|
+
* supported range and whole minutes are carried out of the seconds field.
|
|
99
|
+
*/
|
|
100
|
+
export function capPresetDraft(preset: CountdownPresetDraft): CountdownPresetDraft {
|
|
101
|
+
const minutesText = preset.minutes.trim();
|
|
102
|
+
const secondsText = preset.seconds.trim();
|
|
103
|
+
if (minutesText === '' && secondsText === '') return preset;
|
|
104
|
+
const minutes = Number(minutesText || 0);
|
|
105
|
+
const seconds = Number(secondsText || 0);
|
|
106
|
+
if (!Number.isFinite(minutes) || !Number.isFinite(seconds)) return preset;
|
|
107
|
+
const total = Math.round(minutes * 60 + seconds);
|
|
108
|
+
const capped = Math.max(COUNTDOWN_MIN_SECONDS, Math.min(COUNTDOWN_MAX_SECONDS, total));
|
|
109
|
+
return { minutes: String(Math.floor(capped / 60)), seconds: String(capped % 60) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a preset list to one to five durations. Blank or invalid slots are
|
|
114
|
+
* dropped so a two- or three-preset configuration stays exactly that long.
|
|
115
|
+
* Entries may be raw seconds from settings or minute/second drafts from the UI.
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeCountdownPresets(value: unknown): number[] {
|
|
118
|
+
const input: unknown[] = Array.isArray(value) ? value : [];
|
|
119
|
+
const presets: number[] = [];
|
|
120
|
+
for (const entry of input.slice(0, COUNTDOWN_PRESET_LIMIT)) {
|
|
121
|
+
const seconds = isPresetDraft(entry) ? presetDraftToSeconds(entry) : parseCountdownPreset(entry);
|
|
122
|
+
if (seconds !== null) presets.push(seconds);
|
|
123
|
+
}
|
|
124
|
+
return presets.length > 0 ? presets : [...DEFAULT_COUNTDOWN_PRESETS];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Editable minute/second rows for the countdown preset list. */
|
|
128
|
+
export function createCountdownDraft(value: unknown): CountdownPresetDraft[] {
|
|
129
|
+
return normalizeCountdownPresets(value).map((seconds) => ({
|
|
130
|
+
minutes: String(Math.floor(seconds / 60)),
|
|
131
|
+
seconds: String(seconds % 60),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Fill missing page keys and drop duplicates/unknown values. */
|
|
136
|
+
export function normalizePageOrder(input: unknown): string[] {
|
|
137
|
+
const values: unknown[] = Array.isArray(input) ? input : [];
|
|
138
|
+
const keys = values.filter((key): key is string => typeof key === 'string' && PAGE_ORDER.includes(key));
|
|
139
|
+
const order = [...new Set(keys)];
|
|
140
|
+
for (const key of PAGE_ORDER) if (!order.includes(key)) order.push(key);
|
|
141
|
+
return order.slice(0, PAGE_ORDER.length);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function pageLabels(t: Translator): Record<string, string> {
|
|
145
|
+
return {
|
|
146
|
+
home: t('pageHome'),
|
|
147
|
+
sessions: t('pageSessions'),
|
|
148
|
+
music: t('pageMusic'),
|
|
149
|
+
performance: t('pagePerformance'),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function text(value: unknown): string {
|
|
154
|
+
return value === null || value === undefined ? '' : String(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Build the editable text-field state from the current settings document. */
|
|
158
|
+
export function createDraft(settings: RemoteSettings): DraftState {
|
|
159
|
+
return {
|
|
160
|
+
certPath: settings.tls?.certPath ?? '',
|
|
161
|
+
keyPath: settings.tls?.keyPath ?? '',
|
|
162
|
+
locationName: settings.weather?.name ?? '',
|
|
163
|
+
latitude: text(settings.weather?.latitude),
|
|
164
|
+
longitude: text(settings.weather?.longitude),
|
|
165
|
+
threshold: text(settings.balance?.lowThreshold),
|
|
166
|
+
notificationLimit: String(settings.notifications?.limit ?? 5),
|
|
167
|
+
pomodoroFocus: String(settings.pomodoro?.focusMinutes ?? 25),
|
|
168
|
+
pomodoroBreak: String(settings.pomodoro?.breakMinutes ?? 5),
|
|
169
|
+
pomodoroSound: settings.pomodoro?.soundPath ?? '',
|
|
170
|
+
countdownPresets: createCountdownDraft(settings.countdown?.presets),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Merge a staged patch into the previous one without mutating either. */
|
|
175
|
+
export function mergeStaged(current: StagedSettings, patch: StagedSettings): StagedSettings {
|
|
176
|
+
const merge = (left: unknown, right: unknown): unknown => {
|
|
177
|
+
const next: Record<string, unknown> = { ...(isRecord(left) ? left : {}) };
|
|
178
|
+
for (const [key, value] of Object.entries(isRecord(right) ? right : {})) {
|
|
179
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) next[key] = merge(next[key], value);
|
|
180
|
+
else next[key] = value;
|
|
181
|
+
}
|
|
182
|
+
return next;
|
|
183
|
+
};
|
|
184
|
+
return merge(current, patch) as StagedSettings;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Layer staged edits over the Host settings document for rendering. */
|
|
188
|
+
export function buildSettingsView(base: RemoteSettings, staged: StagedSettings): RemoteSettings {
|
|
189
|
+
return {
|
|
190
|
+
...base,
|
|
191
|
+
...staged,
|
|
192
|
+
weather: { ...(base.weather ?? {}), ...(staged.weather ?? {}) },
|
|
193
|
+
balance: { ...(base.balance ?? {}), ...(staged.balance ?? {}) },
|
|
194
|
+
notifications: { ...(base.notifications ?? {}), ...(staged.notifications ?? {}) },
|
|
195
|
+
tls: { ...(base.tls ?? {}), ...(staged.tls ?? {}) },
|
|
196
|
+
pomodoro: { ...(base.pomodoro ?? {}), ...(staged.pomodoro ?? {}) },
|
|
197
|
+
countdown: { ...(base.countdown ?? {}), ...(staged.countdown ?? {}) },
|
|
198
|
+
pageEnabled: { ...(base.pageEnabled ?? {}), ...(staged.pageEnabled ?? {}) },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface SavePatchInput {
|
|
203
|
+
data: SettingsPayload;
|
|
204
|
+
settings: RemoteSettings;
|
|
205
|
+
draft: DraftState;
|
|
206
|
+
selectedInterfaces: string[];
|
|
207
|
+
selectedApps: string[];
|
|
208
|
+
notificationApps: RemoteApp[];
|
|
209
|
+
order: string[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Minutes are posted as typed; empty or invalid fields keep their default. */
|
|
213
|
+
function minutesOr(value: string, fallback: number): number {
|
|
214
|
+
const text = value.trim();
|
|
215
|
+
if (text === '') return fallback;
|
|
216
|
+
const minutes = Number(text);
|
|
217
|
+
return Number.isFinite(minutes) ? minutes : fallback;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Build the single batch body posted by "save settings". */
|
|
221
|
+
export function buildSavePatch(input: SavePatchInput): SavePatch {
|
|
222
|
+
const { data, settings, draft, selectedInterfaces, selectedApps, notificationApps, order } = input;
|
|
223
|
+
const allInterfaces = [...new Set((data.interfaces ?? []).map((entry) => entry.name))];
|
|
224
|
+
const nextWeather: RemoteSettings['weather'] = {
|
|
225
|
+
...(settings.weather ?? {}),
|
|
226
|
+
name: draft.locationName,
|
|
227
|
+
latitude: draft.latitude === '' ? null : Number(draft.latitude),
|
|
228
|
+
longitude: draft.longitude === '' ? null : Number(draft.longitude),
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
allowedInterfaces: selectedInterfaces.length === allInterfaces.length ? null : selectedInterfaces,
|
|
232
|
+
tokenEnabled: settings.tokenEnabled,
|
|
233
|
+
temperatureUnit: settings.temperatureUnit === 'fahrenheit' ? 'fahrenheit' : 'celsius',
|
|
234
|
+
theme: settings.theme,
|
|
235
|
+
language: settings.language === 'zh' || settings.language === 'en' ? settings.language : 'dsh',
|
|
236
|
+
navPosition: settings.navPosition ?? 'left',
|
|
237
|
+
pomodoro: {
|
|
238
|
+
enabled: Boolean(settings.pomodoro?.enabled),
|
|
239
|
+
focusMinutes: minutesOr(draft.pomodoroFocus, 25),
|
|
240
|
+
breakMinutes: minutesOr(draft.pomodoroBreak, 5),
|
|
241
|
+
soundPath: draft.pomodoroSound,
|
|
242
|
+
loopAlarm: Boolean(settings.pomodoro?.loopAlarm),
|
|
243
|
+
},
|
|
244
|
+
countdown: {
|
|
245
|
+
enabled: Boolean(settings.countdown?.enabled),
|
|
246
|
+
presets: normalizeCountdownPresets(draft.countdownPresets),
|
|
247
|
+
},
|
|
248
|
+
pageOrder: [...order],
|
|
249
|
+
pageEnabled: settings.pageEnabled,
|
|
250
|
+
tls: { enabled: Boolean(settings.tls?.enabled), certPath: draft.certPath, keyPath: draft.keyPath },
|
|
251
|
+
weather: nextWeather,
|
|
252
|
+
balance: {
|
|
253
|
+
spendWindow: settings.balance?.spendWindow ?? 'calendar',
|
|
254
|
+
lowThreshold: draft.threshold === '' ? null : Number(draft.threshold),
|
|
255
|
+
showPeakValley: settings.balance?.showPeakValley !== false,
|
|
256
|
+
},
|
|
257
|
+
notifications: {
|
|
258
|
+
enabledApps: notificationApps.length > 0 && selectedApps.length === notificationApps.length ? [] : selectedApps,
|
|
259
|
+
limit: Number(draft.notificationLimit),
|
|
260
|
+
pushQuestions: Boolean(settings.notifications?.pushQuestions),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Human-readable location-permission result. */
|
|
266
|
+
export function locationHint(t: Translator, result: Pick<LocationResult, 'allowed' | 'reason'>): string {
|
|
267
|
+
if (result.allowed) return t('locationAllowed');
|
|
268
|
+
if (result.reason === 'low-integrity') return t('locationLowIntegrity');
|
|
269
|
+
if (result.reason === 'privacy-denied') return t('locationPrivacyDenied');
|
|
270
|
+
if (result.reason === 'unspecified') return t('locationUnspecified');
|
|
271
|
+
return t('locationFailed');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Human-readable Windows location access state. */
|
|
275
|
+
export function accessLabel(t: Translator, access: string): string {
|
|
276
|
+
if (access === 'allowed') return t('accessAllowed');
|
|
277
|
+
if (access === 'denied') return t('accessDenied');
|
|
278
|
+
if (access === 'unspecified') return t('accessUnspecified');
|
|
279
|
+
return t('accessUnknown');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Compact duration for connected-device age and Node.js uptime. */
|
|
283
|
+
export function formatDuration(ms: number, t: Translator): string {
|
|
284
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
285
|
+
if (seconds < 60) return `${seconds} ${t('durationSeconds')}`;
|
|
286
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} ${t('durationMinutes')}`;
|
|
287
|
+
if (seconds < 86400) {
|
|
288
|
+
const hours = Math.floor(seconds / 3600);
|
|
289
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
290
|
+
return `${hours} ${t('durationHours')} ${minutes} ${t('durationMinutes')}`;
|
|
291
|
+
}
|
|
292
|
+
const days = Math.floor(seconds / 86400);
|
|
293
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
294
|
+
return `${days} ${t('durationDays')} ${hours} ${t('durationHours')}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Byte count with one decimal only when it stays readable. */
|
|
298
|
+
export function formatBytes(value: unknown): string {
|
|
299
|
+
const bytes = Number(value);
|
|
300
|
+
if (!Number.isFinite(bytes) || bytes < 0) return '—';
|
|
301
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
302
|
+
let size = bytes;
|
|
303
|
+
let unit = 0;
|
|
304
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
305
|
+
size /= 1024;
|
|
306
|
+
unit += 1;
|
|
307
|
+
}
|
|
308
|
+
const digits = unit === 0 || size >= 10 ? 0 : 1;
|
|
309
|
+
return `${size.toFixed(digits)} ${units[unit]}`;
|
|
310
|
+
}
|