@joewinke/jatui 0.13.0 → 0.15.0
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/dist/components/VideoPlayer.svelte +779 -16
- package/dist/components/VideoPlayer.svelte.d.ts +108 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +9 -0
- package/dist/video/README.md +3 -3
- package/dist/video/presence/flip.d.ts +145 -0
- package/dist/video/presence/flip.js +253 -0
- package/dist/video/presence/idle.d.ts +82 -0
- package/dist/video/presence/idle.js +145 -0
- package/dist/video/presence/resume.d.ts +76 -0
- package/dist/video/presence/resume.js +139 -0
- package/dist/video/presence/winddown.d.ts +106 -0
- package/dist/video/presence/winddown.js +104 -0
- package/dist/video/presence.d.ts +25 -21
- package/dist/video/presence.js +25 -21
- package/dist/video/rate/ads.d.ts +36 -0
- package/dist/video/rate/ads.js +59 -0
- package/dist/video/rate/compose.d.ts +82 -0
- package/dist/video/rate/compose.js +114 -0
- package/dist/video/rate/silence.d.ts +27 -0
- package/dist/video/rate/silence.js +48 -0
- package/dist/video/rate/speaker.d.ts +54 -0
- package/dist/video/rate/speaker.js +94 -0
- package/dist/video/rate/tick.d.ts +38 -0
- package/dist/video/rate/tick.js +15 -0
- package/dist/video/rate.d.ts +20 -63
- package/dist/video/rate.js +20 -82
- package/dist/video/settings.js +25 -2
- package/package.json +1 -1
|
@@ -4,12 +4,18 @@ export type { VideoLumaTrack } from '../video/band/luma';
|
|
|
4
4
|
export type { VideoMarker } from '../video/band/markers';
|
|
5
5
|
export type { NightMode, NightWindow } from '../video/night/clock';
|
|
6
6
|
export type { AudioState, AudioStatus, AudioBrokenReason } from '../video/audio/graph';
|
|
7
|
+
export type { VideoAdSpan, RateReport } from '../video/rate';
|
|
8
|
+
export type { WindDownState, WindDownPhase, PresenceSource, VideoResumeStore, FlipSource } from '../video/presence';
|
|
9
|
+
export type { ResumePoint } from '../video/advisors/resume-point';
|
|
7
10
|
import type { VideoBookmark, VideoChapter, VideoSpeakerTalkTime, VideoSpeakerTurn, VideoTranscriptSegment, VideoWaveform } from '../video/types';
|
|
8
11
|
import { type BandState } from '../video/band/axis';
|
|
9
12
|
import { type VideoLumaTrack } from '../video/band/luma';
|
|
10
13
|
import { type VideoMarker } from '../video/band/markers';
|
|
14
|
+
import { type RateReport, type VideoAdSpan } from '../video/rate';
|
|
11
15
|
import { type NightMode, type NightWindow } from '../video/night/clock';
|
|
12
16
|
import { type AudioBrokenReason, type AudioState } from '../video/audio';
|
|
17
|
+
import { type FlipSource, type VideoResumeStore, type WindDownState } from '../video/presence';
|
|
18
|
+
import type { ResumePoint } from '../video/advisors/resume-point';
|
|
13
19
|
type $$ComponentProps = {
|
|
14
20
|
src: string;
|
|
15
21
|
poster?: string | null;
|
|
@@ -180,6 +186,108 @@ type $$ComponentProps = {
|
|
|
180
186
|
onAudioState?: (state: AudioState) => void;
|
|
181
187
|
/** Entered `broken`: 'build-failed' | 'unsafe-src' | 'silent' (see above). */
|
|
182
188
|
onAudioBroken?: (reason: AudioBrokenReason) => void;
|
|
189
|
+
/**
|
|
190
|
+
* Rate composer (mos-5lzns.32): `rate = base × speaker × silence`. The
|
|
191
|
+
* speed readout only ever reports `base` — the rate a human chose — so it
|
|
192
|
+
* keeps saying `1×` while a gap is taken at 2.2×. Every advisor is off by
|
|
193
|
+
* default and collapses to 1 until its data arrives: off, the element
|
|
194
|
+
* plays at exactly `base`.
|
|
195
|
+
*
|
|
196
|
+
* Smart speed: run the silences between sentences at 2.2×, found off the
|
|
197
|
+
* `waveform` envelope. Speech stays at `base`.
|
|
198
|
+
*/
|
|
199
|
+
smartSpeed?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Per-speaker pace: level everyone to the median words-per-minute,
|
|
202
|
+
* measured from the diarised `transcript` + `speakers`. Ad reads and
|
|
203
|
+
* gaps between turns play at `base`.
|
|
204
|
+
*/
|
|
205
|
+
speakerPace?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Hand-set speaker rates keyed by speaker NAME (the legend's display
|
|
208
|
+
* name). They always beat measured ones, and like them apply only while
|
|
209
|
+
* `speakerPace` is on — off means off. Range 0.0625–16.
|
|
210
|
+
*/
|
|
211
|
+
speakerRates?: Record<string, number> | null;
|
|
212
|
+
/** Host-detected ad reads. Merged with any `ad: true` speaker turns. */
|
|
213
|
+
adSpans?: VideoAdSpan[] | null;
|
|
214
|
+
/**
|
|
215
|
+
* Skip ad reads: when playback runs into an ad span it jumps to its end
|
|
216
|
+
* (the OSD says so). A seek that lands inside an ad is honoured.
|
|
217
|
+
*/
|
|
218
|
+
skipAds?: boolean;
|
|
219
|
+
/** The composed rate changed (real change only). `base` is what the UI shows. */
|
|
220
|
+
onRateChange?: (report: RateReport) => void;
|
|
221
|
+
/**
|
|
222
|
+
* Wind-down (mos-5lzns.34): when nobody is there, the picture and sound
|
|
223
|
+
* fade over 20s and the video pauses, keeping your place. The fade IS
|
|
224
|
+
* the prompt — any input anywhere on the page (or the desktop coming
|
|
225
|
+
* back) cancels it and brings everything straight back. Omitted/false ⇒
|
|
226
|
+
* the feature does not exist on this player: no listeners, no timers,
|
|
227
|
+
* no variables.
|
|
228
|
+
*
|
|
229
|
+
* The evidence of absence is the DESKTOP when `idleDetection` is on and
|
|
230
|
+
* granted (Chromium's Idle Detection API: system-wide idle, and a locked
|
|
231
|
+
* screen skips the wait entirely); otherwise it is PAGE presence — no
|
|
232
|
+
* input on this page for `windDownAfter` minutes — which is weaker
|
|
233
|
+
* evidence (a video legitimately plays for an hour untouched), and is
|
|
234
|
+
* reported as `source: 'page'` so the host can label it.
|
|
235
|
+
*/
|
|
236
|
+
windDown?: boolean;
|
|
237
|
+
/** Minutes of absence before the fade begins, 10–90 (default 30). */
|
|
238
|
+
windDownAfter?: number;
|
|
239
|
+
/**
|
|
240
|
+
* Only wind down while night is armed (default true — falling asleep
|
|
241
|
+
* mid-video is a night problem). Uses the `night` mode when present,
|
|
242
|
+
* else the local-clock auto window.
|
|
243
|
+
*/
|
|
244
|
+
windDownNightOnly?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Opt in to the Idle Detection API (Chromium only; permission-gated).
|
|
247
|
+
* The player NEVER prompts without this. Already granted ⇒ it starts at
|
|
248
|
+
* once; still `prompt` ⇒ the permission is requested on the next click
|
|
249
|
+
* or key inside the player (the API needs a user gesture). Better: call
|
|
250
|
+
* `requestIdleDetection()` from the click that enables the setting.
|
|
251
|
+
* Unsupported or denied ⇒ the page-presence fallback, reported.
|
|
252
|
+
*/
|
|
253
|
+
idleDetection?: boolean;
|
|
254
|
+
/** Phase/source changes: `off` | `watching` | `fading` | `paused`, and `desktop` | `page`. */
|
|
255
|
+
onWindDown?: (state: WindDownState) => void;
|
|
256
|
+
/**
|
|
257
|
+
* Resume (mos-5lzns.34): present ⇒ the player remembers where this video
|
|
258
|
+
* was left under this key and returns there when it opens again. An
|
|
259
|
+
* explicit `startAt` or a `?t=` deep link always wins over a stored point.
|
|
260
|
+
*/
|
|
261
|
+
resumeKey?: string | null;
|
|
262
|
+
/**
|
|
263
|
+
* Where resume points live — `{ load(key), save(key, sec | null) }`,
|
|
264
|
+
* either may be async. Default: this browser's localStorage. jatui never
|
|
265
|
+
* calls an API; a host with per-user server state passes its own.
|
|
266
|
+
*/
|
|
267
|
+
resumeStore?: VideoResumeStore;
|
|
268
|
+
/**
|
|
269
|
+
* Snap the resume point back to the nearest sentence / speaker turn /
|
|
270
|
+
* chapter within 15s once that data lands, and say the line you're
|
|
271
|
+
* walking back into (quoted, never paraphrased). Stands down if you've
|
|
272
|
+
* moved the playhead yourself. Default on; off ⇒ the raw second.
|
|
273
|
+
*/
|
|
274
|
+
resumeSnap?: boolean;
|
|
275
|
+
/** The player moved the playhead for a resume: `from` the stored second, to `point`. */
|
|
276
|
+
onResume?: (from: number, point: ResumePoint) => void;
|
|
277
|
+
/**
|
|
278
|
+
* Open like an object (mos-5lzns.34): the clicked card (an element or a
|
|
279
|
+
* rect snapshot). Present ⇒ on mount the stage grows out of that
|
|
280
|
+
* rectangle (FLIP over WAAPI, 260ms) with `openPoster` painted over the
|
|
281
|
+
* video until its first frame decodes. `null` ⇒ the poster only. The
|
|
282
|
+
* poster paints on EVERY path — under prefers-reduced-motion, with
|
|
283
|
+
* `openMotion` off, or with the card scrolled away — because a black
|
|
284
|
+
* rectangle is a bug at any motion setting. Omitted ⇒ nothing changes.
|
|
285
|
+
*/
|
|
286
|
+
openFrom?: FlipSource | null;
|
|
287
|
+
/** The card's (already-loaded) thumbnail. Defaults to `poster`. */
|
|
288
|
+
openPoster?: string | null;
|
|
289
|
+
/** Run the motion half of the open (default true). Off ⇒ poster only. */
|
|
290
|
+
openMotion?: boolean;
|
|
183
291
|
class?: string;
|
|
184
292
|
};
|
|
185
293
|
declare const VideoPlayer: import("svelte").Component<$$ComponentProps, {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ export type { ChapterBlock } from './components/replay/ChapterTimeline.svelte';
|
|
|
12
12
|
export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelte';
|
|
13
13
|
export type { TranscriptSegment, TranscriptBookmark, TranscriptChapter, TranscriptSpeakerTalkTime, } from './components/TranscriptPlayer.svelte';
|
|
14
14
|
export { default as VideoPlayer } from './components/VideoPlayer.svelte';
|
|
15
|
-
export type { VideoChapter, VideoSpeakerTurn, VideoTranscriptSegment, VideoBookmark, VideoWaveform, VideoSpeakerTalkTime, VideoLumaTrack, VideoMarker, BandState, BandAxis, NightMode, NightWindow, AudioState, AudioStatus, AudioBrokenReason, } from './components/VideoPlayer.svelte';
|
|
15
|
+
export type { VideoChapter, VideoSpeakerTurn, VideoTranscriptSegment, VideoBookmark, VideoWaveform, VideoSpeakerTalkTime, VideoLumaTrack, VideoMarker, BandState, BandAxis, NightMode, NightWindow, AudioState, AudioStatus, AudioBrokenReason, VideoAdSpan, RateReport, WindDownState, WindDownPhase, PresenceSource, VideoResumeStore, FlipSource, } from './components/VideoPlayer.svelte';
|
|
16
|
+
export { speakerRateMap, toRateTurns, toRateSegments } from './video/rate';
|
|
17
|
+
export { idleDetectionSupported, idleDetectionPermission, requestIdleDetection, localResumeStore, flipOpen, flipClose, clearPoster, type IdlePermission } from './video/presence';
|
|
16
18
|
export { buildSilenceMap, boostAt, estimateSaving, type SilenceMap, type SilenceWave, type SilenceOpts, type BoostOpts } from './video/advisors/smart-speed';
|
|
17
19
|
export { speakerPace, suggestRates, rateAt, describeRates, type SpeakerPace, type SpeakerPaceRow, type RateMap, type RateTurn, type RateSegment, type SuggestOpts } from './video/advisors/speaker-rate';
|
|
18
20
|
export { snapResume, resumeLabel, type ResumePoint, type ResumeReason, type ResumeCtx, type ResumeOpts } from './video/advisors/resume-point';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,15 @@ export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelt
|
|
|
13
13
|
// optional so a bare mp4 still gets the full playback UX. TranscriptPlayer
|
|
14
14
|
// stays exported unchanged — meetings already depends on it.
|
|
15
15
|
export { default as VideoPlayer } from './components/VideoPlayer.svelte';
|
|
16
|
+
// Rate composer (mos-5lzns.32): the same name → rate table the player applies
|
|
17
|
+
// under `speakerPace` (manual beats measured), for a host drawing its own
|
|
18
|
+
// per-speaker speed UI.
|
|
19
|
+
export { speakerRateMap, toRateTurns, toRateSegments } from './video/rate';
|
|
20
|
+
// Presence (mos-5lzns.34): the permission call a host makes from the click
|
|
21
|
+
// that enables wind-down's desktop signal (the API needs a user gesture),
|
|
22
|
+
// the default resume store, and the FLIP pair for a host animating its own
|
|
23
|
+
// theater shell (e.g. flipClose back onto the card).
|
|
24
|
+
export { idleDetectionSupported, idleDetectionPermission, requestIdleDetection, localResumeStore, flipOpen, flipClose, clearPoster } from './video/presence';
|
|
16
25
|
// Pure advisors (mos-5lzns.27): the extension-library self-test modules,
|
|
17
26
|
// ported verbatim. They measure and advise; the player's feature tasks
|
|
18
27
|
// (.31 audio / .32 rate / .34 presence) consume them. qrCanvas is the one
|
package/dist/video/README.md
CHANGED
|
@@ -17,17 +17,17 @@ props + callback surfaces.
|
|
|
17
17
|
| `band.ts` | .28 (done) — extended by **.30** | The timeline band: zoom window (`BandWindow`, `zoomAt`, `followPlayhead`), hit tests (`segmentAt`, `chapterAt`, `biasToSegmentStart`, `nearestBookmark`), and `paintBand` (canvas). .30 adds one-axis arbitration + the light ribbon; its state lives HERE. |
|
|
18
18
|
| `band/` | **.30** (done) | `window.ts` — the zoom-window math (moved out of band.ts, re-exported there; shared by every band layer without an import cycle). `axis.ts` — one-axis four-state arbitration (`resolveAxis`, `BandState`, the churn-guarded microtask `createBandReporter`), `playheadX` (the one "where you are" mapping — ≤0.2px agreement) and `overviewBracket`. `luma.ts` — the light track (`VideoLumaTrack`, the sidecar verbatim) → ribbon (`v² × 0.55`, max-per-interval). `markers.ts` — `VideoMarker` comments (clustered dots) + like heat. |
|
|
19
19
|
| `keys.ts` | .28 (done) | The keyboard map — `PLAYER_KEYS` (the `?` overlay renders it), `chapterJump`, `FRAME_STEP_SEC`. Every key that exists is in the map; a key not in the map doesn't exist. |
|
|
20
|
-
| `rate.ts` | .28
|
|
20
|
+
| `rate.ts` + `rate/` | .28 contract — **.32** (done) | `rate.ts` is the barrel. `compose.ts` — the contract (`composeRate`, clamp [0.0625, 16], `shouldWriteRate`'s 0.001 deadband with an exact return to base), the `SPEED_STEPS` staircase, `formatSpeedLabel`. `speaker.ts` — props → `advisors/speaker-rate`; manual rates keyed by NAME always beat measured. `silence.ts` — waveform → `advisors/smart-speed` map + boost + the OSD saving claim. `ads.ts` — `adSpans` ∪ `ad: true` turns; skip only when PLAYBACK runs into an ad, never when a seek lands in one. `tick.ts` — the component's own 120ms loop's one-tick decision. None of them write `playbackRate`; they advise. |
|
|
21
21
|
| `deepLink.ts` | .28 (done) | `?t=` parse/format/read/write (replaceState, throttled, other params preserved). |
|
|
22
22
|
| `settings.ts` | shared — knobs added by .30–.34 | `coerce` (never throws, never junk) + `VIDEO_SETTINGS` registry. A knob lands WITH its reader. |
|
|
23
23
|
| `audio.ts` + `audio/` | **.31** (done) | `audio.ts` is the barrel. `rider.ts` — pure: RMS dBFS (silence → null), the 12 dB relative-gated loud estimate (rise 0.08 / forget 0.01), want-gain clamped 0.3–1.35, the asymmetric ride (down 0.15 / up 0.02), limiter constants + `compressorMakeupDb`/`limiterTrim` (DynamicsCompressor's automatic makeup, cancelled by a trim node), `routeSafety` (only blob:/data: or a `crossorigin` load — a same-origin URL may redirect cross-origin and route to SILENCE). `graph.ts` — `createAudioSession`: source → gain → quiet chain → limiter → trim → destination, analyser on the source. Context first, route last: the element is taken only when a feature is on, the source is route-safe and `ctx.state === 'running'` (gestures resume + retry; never navigator.userActivation). After routing it watches: an unsafe src or bit-exact zeros while audibly playing ⇒ `broken` (`unsafe-src` / `silent`) via `onBroken`. A construction throw is a permanent `build-failed` (context closed if the element wasn't taken; kept with source → destination if it was). Level off cancels before it sets; limiter moves are ramped. The element never enters the module — the component passes a `route` closure. |
|
|
24
24
|
| `night.ts` + `night/` | **.33** (done) | `night.ts` is the barrel. `meter.ts` — 32×18 frame bytes → Rec.709 luma + blown share, asymmetric smoothing, bias-light cells from the same bytes. `levels.ts` — luma → brightness/saturation/warmth/vignette off `NIGHT_FLOOR` 0.34, blind fixed level, the light-theme surround. `clock.ts` — local-clock `auto` window + the `N` auto/on/off cycle (no hyprsunset). `track.ts` — prediction (only ever adds dimming), flash intervals, `composeNight` (darker at once, brighter released per tick — never a jump). |
|
|
25
|
-
| `presence.ts` | **.34** |
|
|
25
|
+
| `presence.ts` + `presence/` | **.34** (done) | `presence.ts` is the barrel. `winddown.ts` — pure: `shouldBeginWindDown` (a locked screen skips the wait; a desktop source with no reading holds — never guess), the 20s linear fade levels (volume `1−p`, picture `1−0.92p`), the hint (the page source says it is guessing from less). `idle.ts` — Chromium's Idle Detection API, feature-detected (`IdleDetector` + secure context); `startDesktopWatch` never prompts (start() rejects unless already granted ⇒ page fallback), `requestIdleDetection` is the ONE prompting call and the player makes it only with `idleDetection` on, from a gesture inside it. `resume.ts` — the host-supplied `VideoResumeStore` (localStorage default; jatui never calls an API), the data → advisor mapping, and `planResumeSnap` (wait for boundaries, drop once the user moved > 2.5s, skip `exact`/`start`); the snapping itself is `advisors/resume-point`, not re-derived. `flip.ts` — `open-transition.js` ported: FLIP over WAAPI, two keyframes, origin restated in both, poster on every path, a no-op under prefers-reduced-motion except the poster, `flipClose` holds and the next open releases on every path. |
|
|
26
26
|
| `advisors/` | **.27 (done)** | The five extension-library pure advisors, ported verbatim: `smart-speed` (silence map + boost), `speaker-rate` (measured wpm + suggested rates), `resume-point` (sentence-snapping + the label), `quiet-listening` (tone-only chain + `amountFor`), `qr` (ISO 18004 encoder v1–10). Each has its own README row in `advisors/README.md`; the `--self-test` suites live on as vitest in `tests/unit/advisors/` (`npm run test:advisors`). |
|
|
27
27
|
|
|
28
28
|
Sub-task map: **.30** scrubbing/one-axis/ribbon/markers → `band.ts` (+ its own
|
|
29
29
|
paint layers); **.31** audio graph → `audio.ts`; **.32** rate composer →
|
|
30
|
-
`rate.ts`
|
|
30
|
+
`rate.ts` + `rate/`; **.33** night → `night.ts`; **.34** presence/wind-down →
|
|
31
31
|
`presence.ts`. **.27** (landed) ports the advisors themselves — .31's chain
|
|
32
32
|
builder and `amountFor` live in `advisors/quiet-listening`, .32's two advisor
|
|
33
33
|
modules in `advisors/smart-speed` + `advisors/speaker-rate`, .34's snapping in
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open like an object (mos-5lzns.34) — extension-library
|
|
3
|
+
* `lib/open-transition.js`, ported. The card you pointed at becomes the
|
|
4
|
+
* thing you're watching.
|
|
5
|
+
*
|
|
6
|
+
* Opening a video read as a cut, for two different reasons:
|
|
7
|
+
* · the rectangle is BLACK because <video> paints its own opaque letterbox
|
|
8
|
+
* before it has a frame — fix: paint the card's (already-loaded) thumbnail
|
|
9
|
+
* over that box on the same frame the player appears, so there is no
|
|
10
|
+
* instant at which the player is empty;
|
|
11
|
+
* · it is a CUT because nothing carries the eye — fix: move the shell from
|
|
12
|
+
* the card's rectangle to its own.
|
|
13
|
+
*
|
|
14
|
+
* flipOpen({ from, shell, poster?, hasPoster?, duration? }) -> Promise<void>
|
|
15
|
+
* flipClose({ to, shell, duration? }) -> Promise<void>
|
|
16
|
+
* clearPoster(shell) — call on the video's `loadeddata`
|
|
17
|
+
*
|
|
18
|
+
* FLIP (read both rects, invert with a transform, play), never an animated
|
|
19
|
+
* box: width/height/top/left is a layout property per frame. WAAPI, never
|
|
20
|
+
* CSS classes: it works with no stylesheet cooperation and never writes the
|
|
21
|
+
* `style` attribute, so "no inline styles left behind" is structural — the
|
|
22
|
+
* `will-change` hint is reclaimed by name on finish AND cancel.
|
|
23
|
+
*
|
|
24
|
+
* `transformOrigin: 0 0` is restated in BOTH keyframes (a property named in
|
|
25
|
+
* only one keyframe interpolates toward the default and silently breaks the
|
|
26
|
+
* translate/scale pair). Two keyframes, never three: `easing` is the
|
|
27
|
+
* effect's timing function, so a hand-placed offset lands somewhere other
|
|
28
|
+
* than where it reads. The scale is non-uniform on purpose — matching the
|
|
29
|
+
* card's outline exactly is what hides the seam.
|
|
30
|
+
*
|
|
31
|
+
* Every entry point is a no-op returning a resolved Promise when it has
|
|
32
|
+
* nothing honest to animate — no source, a source scrolled off screen, an
|
|
33
|
+
* unmeasurable shell, `prefers-reduced-motion`, or geometry already
|
|
34
|
+
* congruent within 1px / 1%. THE POSTER IS THE EXCEPTION: it is painted on
|
|
35
|
+
* every one of those paths, because a black rectangle is a bug at any
|
|
36
|
+
* motion setting, not a flourish someone opted out of.
|
|
37
|
+
*
|
|
38
|
+
* `flipClose` holds its end state (`fill: forwards`) — snapping back to full
|
|
39
|
+
* size on the last frame would flash the whole player right before the host
|
|
40
|
+
* hides it. The hold is released by the next `flipOpen` on EVERY path,
|
|
41
|
+
* including the no-op ones, so a shell reopened under reduced motion can
|
|
42
|
+
* never sit shrunk at the last card's position.
|
|
43
|
+
*
|
|
44
|
+
* The one DOM-touching module in presence/ besides idle.ts; its globals are
|
|
45
|
+
* injectable (`g`) so node:test drives it with fakes.
|
|
46
|
+
*/
|
|
47
|
+
/** The app's one state-change curve. */
|
|
48
|
+
export declare const FLIP_EASE = "cubic-bezier(.25, 1, .5, 1)";
|
|
49
|
+
export declare const FLIP_OPEN_MS = 260;
|
|
50
|
+
export declare const FLIP_CLOSE_MS = 200;
|
|
51
|
+
/** Opacity the shell starts at: shallow with a picture on screen, deeper without. */
|
|
52
|
+
export declare const FLIP_FADE_WITH_POSTER = 0.7;
|
|
53
|
+
export declare const FLIP_FADE_BARE = 0.25;
|
|
54
|
+
export declare const FLIP_POSTER_CLASS = "jatui-flip-poster";
|
|
55
|
+
/** Anything with a rectangle: an Element, a DOMRect, or a snapshot of one. */
|
|
56
|
+
export type FlipSource = {
|
|
57
|
+
getBoundingClientRect(): {
|
|
58
|
+
left: number;
|
|
59
|
+
top: number;
|
|
60
|
+
width: number;
|
|
61
|
+
height: number;
|
|
62
|
+
};
|
|
63
|
+
} | {
|
|
64
|
+
left: number;
|
|
65
|
+
top: number;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
};
|
|
69
|
+
export interface FlipRect {
|
|
70
|
+
left: number;
|
|
71
|
+
top: number;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
}
|
|
75
|
+
type Globals = {
|
|
76
|
+
matchMedia?: (q: string) => {
|
|
77
|
+
matches: boolean;
|
|
78
|
+
};
|
|
79
|
+
innerWidth?: number;
|
|
80
|
+
innerHeight?: number;
|
|
81
|
+
document?: {
|
|
82
|
+
createElement(tag: string): unknown;
|
|
83
|
+
};
|
|
84
|
+
getComputedStyle?: (el: unknown) => {
|
|
85
|
+
position?: string;
|
|
86
|
+
borderRadius?: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Duck-types an Element or a rect snapshot (a virtualised grid may hand over
|
|
91
|
+
* a rect it measured before the card went away). Zero-sized or non-finite ⇒
|
|
92
|
+
* null — never divide a scale factor by nothing.
|
|
93
|
+
*/
|
|
94
|
+
export declare function rectOf(x: unknown): FlipRect | null;
|
|
95
|
+
/** Any part of the rect inside the viewport. Unknown viewport ⇒ assume visible. */
|
|
96
|
+
export declare function onScreen(r: FlipRect, vw: unknown, vh: unknown): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The inversion — "put my corner on the card's corner, shrink to its size".
|
|
99
|
+
* Null when the boxes already coincide (within 1px / 1%) or can't be scaled.
|
|
100
|
+
*/
|
|
101
|
+
export declare function flipGeometry(visual: FlipRect, layout: FlipRect): {
|
|
102
|
+
dx: number;
|
|
103
|
+
dy: number;
|
|
104
|
+
sx: number;
|
|
105
|
+
sy: number;
|
|
106
|
+
} | null;
|
|
107
|
+
export declare function awayFrame(g: {
|
|
108
|
+
dx: number;
|
|
109
|
+
dy: number;
|
|
110
|
+
sx: number;
|
|
111
|
+
sy: number;
|
|
112
|
+
}): Keyframe;
|
|
113
|
+
export declare function homeFrame(): Keyframe;
|
|
114
|
+
/**
|
|
115
|
+
* A poster string → a safe `background-image` value, or "" for none. A bare
|
|
116
|
+
* URL containing a quote, paren or backslash emits nothing rather than a
|
|
117
|
+
* broken declaration that would take the whole rule with it.
|
|
118
|
+
*/
|
|
119
|
+
export declare function posterBackground(poster: unknown): string;
|
|
120
|
+
/** Remove a poster painted by flipOpen (and the position it may have needed). */
|
|
121
|
+
export declare function clearPoster(shell: unknown): void;
|
|
122
|
+
/**
|
|
123
|
+
* Grow `shell` out of `from`'s rectangle. `poster` (a URL or CSS image) is
|
|
124
|
+
* painted into the shell on EVERY path; a host that paints its own poster
|
|
125
|
+
* passes `hasPoster: true` instead so the fade depth matches.
|
|
126
|
+
*/
|
|
127
|
+
export declare function flipOpen(opts?: {
|
|
128
|
+
from?: FlipSource | null;
|
|
129
|
+
shell?: unknown;
|
|
130
|
+
poster?: string | null;
|
|
131
|
+
hasPoster?: boolean;
|
|
132
|
+
duration?: number;
|
|
133
|
+
g?: Globals;
|
|
134
|
+
}): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Shrink `shell` back onto `to` and HOLD there (the host hides it after).
|
|
137
|
+
* No opacity: the last frame before the host hides the shell IS the card.
|
|
138
|
+
*/
|
|
139
|
+
export declare function flipClose(opts?: {
|
|
140
|
+
to?: FlipSource | null;
|
|
141
|
+
shell?: unknown;
|
|
142
|
+
duration?: number;
|
|
143
|
+
g?: Globals;
|
|
144
|
+
}): Promise<void>;
|
|
145
|
+
export {};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open like an object (mos-5lzns.34) — extension-library
|
|
3
|
+
* `lib/open-transition.js`, ported. The card you pointed at becomes the
|
|
4
|
+
* thing you're watching.
|
|
5
|
+
*
|
|
6
|
+
* Opening a video read as a cut, for two different reasons:
|
|
7
|
+
* · the rectangle is BLACK because <video> paints its own opaque letterbox
|
|
8
|
+
* before it has a frame — fix: paint the card's (already-loaded) thumbnail
|
|
9
|
+
* over that box on the same frame the player appears, so there is no
|
|
10
|
+
* instant at which the player is empty;
|
|
11
|
+
* · it is a CUT because nothing carries the eye — fix: move the shell from
|
|
12
|
+
* the card's rectangle to its own.
|
|
13
|
+
*
|
|
14
|
+
* flipOpen({ from, shell, poster?, hasPoster?, duration? }) -> Promise<void>
|
|
15
|
+
* flipClose({ to, shell, duration? }) -> Promise<void>
|
|
16
|
+
* clearPoster(shell) — call on the video's `loadeddata`
|
|
17
|
+
*
|
|
18
|
+
* FLIP (read both rects, invert with a transform, play), never an animated
|
|
19
|
+
* box: width/height/top/left is a layout property per frame. WAAPI, never
|
|
20
|
+
* CSS classes: it works with no stylesheet cooperation and never writes the
|
|
21
|
+
* `style` attribute, so "no inline styles left behind" is structural — the
|
|
22
|
+
* `will-change` hint is reclaimed by name on finish AND cancel.
|
|
23
|
+
*
|
|
24
|
+
* `transformOrigin: 0 0` is restated in BOTH keyframes (a property named in
|
|
25
|
+
* only one keyframe interpolates toward the default and silently breaks the
|
|
26
|
+
* translate/scale pair). Two keyframes, never three: `easing` is the
|
|
27
|
+
* effect's timing function, so a hand-placed offset lands somewhere other
|
|
28
|
+
* than where it reads. The scale is non-uniform on purpose — matching the
|
|
29
|
+
* card's outline exactly is what hides the seam.
|
|
30
|
+
*
|
|
31
|
+
* Every entry point is a no-op returning a resolved Promise when it has
|
|
32
|
+
* nothing honest to animate — no source, a source scrolled off screen, an
|
|
33
|
+
* unmeasurable shell, `prefers-reduced-motion`, or geometry already
|
|
34
|
+
* congruent within 1px / 1%. THE POSTER IS THE EXCEPTION: it is painted on
|
|
35
|
+
* every one of those paths, because a black rectangle is a bug at any
|
|
36
|
+
* motion setting, not a flourish someone opted out of.
|
|
37
|
+
*
|
|
38
|
+
* `flipClose` holds its end state (`fill: forwards`) — snapping back to full
|
|
39
|
+
* size on the last frame would flash the whole player right before the host
|
|
40
|
+
* hides it. The hold is released by the next `flipOpen` on EVERY path,
|
|
41
|
+
* including the no-op ones, so a shell reopened under reduced motion can
|
|
42
|
+
* never sit shrunk at the last card's position.
|
|
43
|
+
*
|
|
44
|
+
* The one DOM-touching module in presence/ besides idle.ts; its globals are
|
|
45
|
+
* injectable (`g`) so node:test drives it with fakes.
|
|
46
|
+
*/
|
|
47
|
+
/** The app's one state-change curve. */
|
|
48
|
+
export const FLIP_EASE = 'cubic-bezier(.25, 1, .5, 1)';
|
|
49
|
+
export const FLIP_OPEN_MS = 260;
|
|
50
|
+
export const FLIP_CLOSE_MS = 200;
|
|
51
|
+
/** Opacity the shell starts at: shallow with a picture on screen, deeper without. */
|
|
52
|
+
export const FLIP_FADE_WITH_POSTER = 0.7;
|
|
53
|
+
export const FLIP_FADE_BARE = 0.25;
|
|
54
|
+
export const FLIP_POSTER_CLASS = 'jatui-flip-poster';
|
|
55
|
+
const G = () => (typeof globalThis === 'undefined' ? {} : globalThis);
|
|
56
|
+
// Keyed on the shell, not a field on an element the host owns.
|
|
57
|
+
const running = new WeakMap();
|
|
58
|
+
function reducedMotion(g) {
|
|
59
|
+
try {
|
|
60
|
+
return typeof g.matchMedia === 'function' && g.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function animatable(el) {
|
|
67
|
+
return !!el && typeof el.animate === 'function' && typeof el.getBoundingClientRect === 'function';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Duck-types an Element or a rect snapshot (a virtualised grid may hand over
|
|
71
|
+
* a rect it measured before the card went away). Zero-sized or non-finite ⇒
|
|
72
|
+
* null — never divide a scale factor by nothing.
|
|
73
|
+
*/
|
|
74
|
+
export function rectOf(x) {
|
|
75
|
+
if (!x || typeof x !== 'object')
|
|
76
|
+
return null;
|
|
77
|
+
const src = x;
|
|
78
|
+
let r;
|
|
79
|
+
try {
|
|
80
|
+
r =
|
|
81
|
+
typeof src.getBoundingClientRect === 'function'
|
|
82
|
+
? src.getBoundingClientRect()
|
|
83
|
+
: src;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!r)
|
|
89
|
+
return null;
|
|
90
|
+
const width = Number(r.width);
|
|
91
|
+
const height = Number(r.height);
|
|
92
|
+
const left = Number(r.left);
|
|
93
|
+
const top = Number(r.top);
|
|
94
|
+
if (!(width > 0) || !(height > 0) || !Number.isFinite(left) || !Number.isFinite(top))
|
|
95
|
+
return null;
|
|
96
|
+
return { left, top, width, height };
|
|
97
|
+
}
|
|
98
|
+
/** Any part of the rect inside the viewport. Unknown viewport ⇒ assume visible. */
|
|
99
|
+
export function onScreen(r, vw, vh) {
|
|
100
|
+
const w = Number(vw);
|
|
101
|
+
const h = Number(vh);
|
|
102
|
+
if (!Number.isFinite(w) || !Number.isFinite(h))
|
|
103
|
+
return true;
|
|
104
|
+
return r.left < w && r.top < h && r.left + r.width > 0 && r.top + r.height > 0;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The inversion — "put my corner on the card's corner, shrink to its size".
|
|
108
|
+
* Null when the boxes already coincide (within 1px / 1%) or can't be scaled.
|
|
109
|
+
*/
|
|
110
|
+
export function flipGeometry(visual, layout) {
|
|
111
|
+
const sx = visual.width / layout.width;
|
|
112
|
+
const sy = visual.height / layout.height;
|
|
113
|
+
const dx = visual.left - layout.left;
|
|
114
|
+
const dy = visual.top - layout.top;
|
|
115
|
+
if (![sx, sy, dx, dy].every(Number.isFinite) || !(sx > 0) || !(sy > 0))
|
|
116
|
+
return null;
|
|
117
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1 && Math.abs(sx - 1) < 0.01 && Math.abs(sy - 1) < 0.01)
|
|
118
|
+
return null;
|
|
119
|
+
return { dx, dy, sx, sy };
|
|
120
|
+
}
|
|
121
|
+
export function awayFrame(g) {
|
|
122
|
+
return { transformOrigin: '0 0', transform: `translate(${g.dx}px, ${g.dy}px) scale(${g.sx}, ${g.sy})` };
|
|
123
|
+
}
|
|
124
|
+
export function homeFrame() {
|
|
125
|
+
return { transformOrigin: '0 0', transform: 'none' };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A poster string → a safe `background-image` value, or "" for none. A bare
|
|
129
|
+
* URL containing a quote, paren or backslash emits nothing rather than a
|
|
130
|
+
* broken declaration that would take the whole rule with it.
|
|
131
|
+
*/
|
|
132
|
+
export function posterBackground(poster) {
|
|
133
|
+
if (typeof poster !== 'string')
|
|
134
|
+
return '';
|
|
135
|
+
const v = poster.trim();
|
|
136
|
+
if (!v)
|
|
137
|
+
return '';
|
|
138
|
+
if (/^(url\(|none$|(-webkit-)?(linear|radial|conic)-gradient\(|(-webkit-)?image-set\()/i.test(v)) {
|
|
139
|
+
return v.toLowerCase() === 'none' ? '' : v;
|
|
140
|
+
}
|
|
141
|
+
if (/["'()\\]/.test(v))
|
|
142
|
+
return '';
|
|
143
|
+
return `url("${v}")`;
|
|
144
|
+
}
|
|
145
|
+
/** Remove a poster painted by flipOpen (and the position it may have needed). */
|
|
146
|
+
export function clearPoster(shell) {
|
|
147
|
+
const s = shell;
|
|
148
|
+
if (!s || typeof s.querySelector !== 'function')
|
|
149
|
+
return;
|
|
150
|
+
const el = s.querySelector('.' + FLIP_POSTER_CLASS);
|
|
151
|
+
if (!el)
|
|
152
|
+
return;
|
|
153
|
+
if (el.dataset?.restorePosition === '1')
|
|
154
|
+
s.style.position = '';
|
|
155
|
+
el.remove?.();
|
|
156
|
+
}
|
|
157
|
+
function paintPoster(shell, poster, g) {
|
|
158
|
+
if (!shell || typeof shell.appendChild !== 'function' || !g.document)
|
|
159
|
+
return false;
|
|
160
|
+
clearPoster(shell); // a posterless reopen must not keep the last video's picture
|
|
161
|
+
const image = posterBackground(poster);
|
|
162
|
+
if (!image)
|
|
163
|
+
return false;
|
|
164
|
+
const el = g.document.createElement('div');
|
|
165
|
+
el.className = FLIP_POSTER_CLASS;
|
|
166
|
+
el.setAttribute('aria-hidden', 'true');
|
|
167
|
+
el.style.cssText =
|
|
168
|
+
'position:absolute;inset:0;z-index:0;pointer-events:none;' +
|
|
169
|
+
'background-repeat:no-repeat;background-position:center;background-size:contain;';
|
|
170
|
+
el.style.backgroundImage = image;
|
|
171
|
+
const cs = g.getComputedStyle;
|
|
172
|
+
if (typeof cs === 'function' && cs(shell).position === 'static') {
|
|
173
|
+
shell.style.position = 'relative';
|
|
174
|
+
el.dataset.restorePosition = '1';
|
|
175
|
+
}
|
|
176
|
+
shell.appendChild(el);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
function release(shell) {
|
|
180
|
+
const prev = running.get(shell);
|
|
181
|
+
if (!prev)
|
|
182
|
+
return;
|
|
183
|
+
running.delete(shell);
|
|
184
|
+
try {
|
|
185
|
+
prev.cancel();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* already gone */
|
|
189
|
+
}
|
|
190
|
+
shell.style.willChange = '';
|
|
191
|
+
}
|
|
192
|
+
function play(shell, frames, duration, hold) {
|
|
193
|
+
const anim = shell.animate(frames, { duration, easing: FLIP_EASE, fill: hold ? 'forwards' : 'none' });
|
|
194
|
+
running.set(shell, anim);
|
|
195
|
+
shell.style.willChange = 'transform'; // promoted for the quarter second it earns it, never longer
|
|
196
|
+
const settle = () => {
|
|
197
|
+
if (running.get(shell) !== anim)
|
|
198
|
+
return; // a later open already owns the shell
|
|
199
|
+
if (!hold)
|
|
200
|
+
running.delete(shell);
|
|
201
|
+
shell.style.willChange = '';
|
|
202
|
+
};
|
|
203
|
+
return anim.finished.then(settle, settle);
|
|
204
|
+
}
|
|
205
|
+
const durationOf = (v, fallback) => {
|
|
206
|
+
const n = Number(v);
|
|
207
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Grow `shell` out of `from`'s rectangle. `poster` (a URL or CSS image) is
|
|
211
|
+
* painted into the shell on EVERY path; a host that paints its own poster
|
|
212
|
+
* passes `hasPoster: true` instead so the fade depth matches.
|
|
213
|
+
*/
|
|
214
|
+
export function flipOpen(opts = {}) {
|
|
215
|
+
const g = opts.g ?? G();
|
|
216
|
+
const shell = opts.shell;
|
|
217
|
+
const painted = paintPoster(shell, opts.poster, g) || opts.hasPoster === true;
|
|
218
|
+
if (!animatable(shell))
|
|
219
|
+
return Promise.resolve();
|
|
220
|
+
release(shell); // on every path — a held close must never survive a reopen
|
|
221
|
+
if (reducedMotion(g))
|
|
222
|
+
return Promise.resolve();
|
|
223
|
+
const fromRect = rectOf(opts.from);
|
|
224
|
+
const shellRect = rectOf(shell);
|
|
225
|
+
if (!fromRect || !shellRect || !onScreen(fromRect, g.innerWidth, g.innerHeight))
|
|
226
|
+
return Promise.resolve();
|
|
227
|
+
const geo = flipGeometry(fromRect, shellRect);
|
|
228
|
+
if (!geo)
|
|
229
|
+
return Promise.resolve();
|
|
230
|
+
const fade = painted ? FLIP_FADE_WITH_POSTER : FLIP_FADE_BARE;
|
|
231
|
+
return play(shell, [{ ...awayFrame(geo), opacity: fade }, { ...homeFrame(), opacity: 1 }], durationOf(opts.duration, FLIP_OPEN_MS), false);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Shrink `shell` back onto `to` and HOLD there (the host hides it after).
|
|
235
|
+
* No opacity: the last frame before the host hides the shell IS the card.
|
|
236
|
+
*/
|
|
237
|
+
export function flipClose(opts = {}) {
|
|
238
|
+
const g = opts.g ?? G();
|
|
239
|
+
const shell = opts.shell;
|
|
240
|
+
if (!animatable(shell))
|
|
241
|
+
return Promise.resolve();
|
|
242
|
+
release(shell);
|
|
243
|
+
if (reducedMotion(g))
|
|
244
|
+
return Promise.resolve();
|
|
245
|
+
const toRect = rectOf(opts.to);
|
|
246
|
+
const shellRect = rectOf(shell);
|
|
247
|
+
if (!toRect || !shellRect || !onScreen(toRect, g.innerWidth, g.innerHeight))
|
|
248
|
+
return Promise.resolve();
|
|
249
|
+
const geo = flipGeometry(toRect, shellRect);
|
|
250
|
+
if (!geo)
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
return play(shell, [homeFrame(), awayFrame(geo)], durationOf(opts.duration, FLIP_CLOSE_MS), true);
|
|
253
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desktop signal for wind-down (mos-5lzns.34): Chromium's Idle Detection
|
|
3
|
+
* API (`IdleDetector`) — system-wide user idle + screen locked, the browser's
|
|
4
|
+
* near-exact equivalent of the extension's `hyprctl cursorpos` + `hyprlock`.
|
|
5
|
+
*
|
|
6
|
+
* Three rules, all load-bearing:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Feature-detect, never assume.** `IdleDetector` exists only in
|
|
9
|
+
* Chromium, only in a secure context, and only where the
|
|
10
|
+
* `idle-detection` permissions policy allows it (a cross-origin iframe
|
|
11
|
+
* without `allow="idle-detection"` is refused). Every entry point here
|
|
12
|
+
* fails toward "no desktop signal", which the caller turns into the
|
|
13
|
+
* labelled page-presence fallback — never into a guess.
|
|
14
|
+
* 2. **Never prompt uninvited.** `requestIdleDetection()` is the ONLY call
|
|
15
|
+
* that can surface a permission prompt, and it is only ever made because
|
|
16
|
+
* the host opted in (the player's `idleDetection` prop). It needs a user
|
|
17
|
+
* gesture (transient activation) — call it from the click that enables
|
|
18
|
+
* the setting. `startDesktopWatch()` never asks: it starts only when the
|
|
19
|
+
* permission is already granted.
|
|
20
|
+
* 3. **The caller decides.** Like the extension's host (`desktop_idle()`
|
|
21
|
+
* reports two raw facts and keeps no state), this reports `{ idle,
|
|
22
|
+
* locked }` and nothing else. How long the video has been playing, and
|
|
23
|
+
* whether that adds up to absence, is the component's call
|
|
24
|
+
* (./winddown.ts).
|
|
25
|
+
*
|
|
26
|
+
* The global is injectable (`g`) so node:test can drive a fake detector —
|
|
27
|
+
* the same seam the audio graph uses for AudioContext.
|
|
28
|
+
*/
|
|
29
|
+
import type { DesktopSignal } from './winddown';
|
|
30
|
+
/** The API's floor: `threshold` below 60s is rejected by the browser. */
|
|
31
|
+
export declare const IDLE_THRESHOLD_MIN_MS = 60000;
|
|
32
|
+
export type IdlePermission = 'granted' | 'denied' | 'prompt' | 'unsupported';
|
|
33
|
+
type Globals = {
|
|
34
|
+
IdleDetector?: unknown;
|
|
35
|
+
isSecureContext?: boolean;
|
|
36
|
+
navigator?: {
|
|
37
|
+
permissions?: {
|
|
38
|
+
query?: (d: {
|
|
39
|
+
name: string;
|
|
40
|
+
}) => Promise<{
|
|
41
|
+
state: string;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
AbortController?: typeof AbortController;
|
|
46
|
+
};
|
|
47
|
+
/** Does this browser have the Idle Detection API at all? (Chromium, secure context.) */
|
|
48
|
+
export declare function idleDetectionSupported(g?: Globals | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The current permission state WITHOUT prompting. `'unsupported'` when the
|
|
51
|
+
* API or the Permissions API is missing, or the query throws (an unknown
|
|
52
|
+
* permission name throws in browsers that don't know it).
|
|
53
|
+
*/
|
|
54
|
+
export declare function idleDetectionPermission(g?: Globals | undefined): Promise<IdlePermission>;
|
|
55
|
+
/**
|
|
56
|
+
* Ask for the permission. MUST be called from a user gesture, and only
|
|
57
|
+
* because the user opted in — this is the one call that can show a prompt.
|
|
58
|
+
* Resolves `'granted'` / `'denied'`, or `'unsupported'` (no API, or the
|
|
59
|
+
* browser refused to ask: no activation, policy-blocked frame).
|
|
60
|
+
*/
|
|
61
|
+
export declare function requestIdleDetection(g?: Globals | undefined): Promise<Exclude<IdlePermission, 'prompt'>>;
|
|
62
|
+
export interface DesktopWatch {
|
|
63
|
+
/** Resolves true once the detector is running; false if it could not start. */
|
|
64
|
+
ready: Promise<boolean>;
|
|
65
|
+
/** Latest reading, or null before the first one. */
|
|
66
|
+
read(): DesktopSignal | null;
|
|
67
|
+
stop(): void;
|
|
68
|
+
}
|
|
69
|
+
/** Map the API's two strings onto the two facts. Unknown ⇒ null (no reading). */
|
|
70
|
+
export declare function toDesktopSignal(userState: unknown, screenState: unknown): DesktopSignal | null;
|
|
71
|
+
/**
|
|
72
|
+
* Start watching the desktop. Never prompts: `start()` rejects with
|
|
73
|
+
* NotAllowedError unless the permission is already granted, and that
|
|
74
|
+
* rejection becomes `ready → false`. `thresholdMs` is clamped up to the
|
|
75
|
+
* API's 60s floor. `onChange` fires with each new reading.
|
|
76
|
+
*/
|
|
77
|
+
export declare function startDesktopWatch(opts: {
|
|
78
|
+
thresholdMs: number;
|
|
79
|
+
onChange: (signal: DesktopSignal | null) => void;
|
|
80
|
+
g?: Globals;
|
|
81
|
+
}): DesktopWatch;
|
|
82
|
+
export {};
|