98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { App } from "@capacitor/app";
|
|
import { Capacitor } from "@capacitor/core";
|
|
import { create } from "zustand";
|
|
|
|
/** A stretch of time the player was away: the game hidden, in the background, or closed. */
|
|
export interface Absence {
|
|
from: number;
|
|
to: number;
|
|
}
|
|
|
|
/**
|
|
* Whether the player is "here" (decision 0.5.160): the tab visible or the app
|
|
* in the foreground. `lastReturn` is the absence that ended most recently
|
|
* (a cold start counts as coming back from the save's last time seen);
|
|
* `returns` counts comebacks, so a component can react to each one.
|
|
*/
|
|
export interface PresenceState {
|
|
here: boolean;
|
|
awaySince: number | null;
|
|
lastReturn: Absence | null;
|
|
returns: number;
|
|
}
|
|
|
|
/** The state when the game starts: back from `savedAt` (null for a new game) if it opens in front. */
|
|
export function presenceAtStart(here: boolean, now: number, savedAt: number | null): PresenceState {
|
|
if (!here) return { here, awaySince: savedAt ?? now, lastReturn: null, returns: 0 };
|
|
return {
|
|
here,
|
|
awaySince: null,
|
|
lastReturn: savedAt !== null && savedAt < now ? { from: savedAt, to: now } : null,
|
|
returns: savedAt !== null && savedAt < now ? 1 : 0,
|
|
};
|
|
}
|
|
|
|
/** Going away or coming back at `now`; anything else changes nothing. */
|
|
export function presenceChange(state: PresenceState, here: boolean, now: number): PresenceState {
|
|
if (here === state.here) return state;
|
|
if (!here) return { ...state, here, awaySince: now };
|
|
return {
|
|
here,
|
|
awaySince: null,
|
|
lastReturn: { from: state.awaySince ?? now, to: now },
|
|
returns: state.returns + 1,
|
|
};
|
|
}
|
|
|
|
export const usePresence = create<PresenceState>(() => ({
|
|
here: true,
|
|
awaySince: null,
|
|
lastReturn: null,
|
|
returns: 0,
|
|
}));
|
|
|
|
/** Where "here" comes from: the page's visibility, or the native app's state. */
|
|
export interface PresenceHost {
|
|
isHere(): boolean;
|
|
/** Calls `fn` with each change; returns an unsubscribe. */
|
|
onChange(fn: (here: boolean) => void): () => void;
|
|
}
|
|
|
|
/** The browser tab (Page Visibility API). */
|
|
export const browserPresence: PresenceHost = {
|
|
isHere: () => document.visibilityState === "visible",
|
|
onChange(fn) {
|
|
const listener = () => fn(document.visibilityState === "visible");
|
|
document.addEventListener("visibilitychange", listener);
|
|
return () => document.removeEventListener("visibilitychange", listener);
|
|
},
|
|
};
|
|
|
|
/** The native app (Capacitor app state): in the foreground or not. */
|
|
export const nativePresence: PresenceHost = {
|
|
isHere: () => document.visibilityState === "visible",
|
|
onChange(fn) {
|
|
const handle = App.addListener("appStateChange", ({ isActive }) => fn(isActive));
|
|
return () => void handle.then((h) => h.remove());
|
|
},
|
|
};
|
|
|
|
export function platformPresence(): PresenceHost {
|
|
return Capacitor.isNativePlatform() ? nativePresence : browserPresence;
|
|
}
|
|
|
|
/**
|
|
* Tracks presence into `usePresence` (CR-028 Step 28.2), with times from the
|
|
* game's clock so they compare with logbook times. Returns a stop function.
|
|
*/
|
|
export function startPresence(
|
|
host: PresenceHost,
|
|
now: () => number,
|
|
savedAt: number | null,
|
|
): () => void {
|
|
usePresence.setState(presenceAtStart(host.isHere(), now(), savedAt));
|
|
return host.onChange((here) => {
|
|
usePresence.setState((state) => presenceChange(state, here, now()));
|
|
});
|
|
}
|