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