Skip to content
Source
Sign inSign up

previewMiniPlayerStore.ts

159 lines4.9 KB

1import { scopedThreadKey } from "@t3tools/client-runtime/environment";
2import type { DevicePlatform, ScopedThreadRef } from "@t3tools/contracts";
3import { create } from "zustand";
4​
5export interface PreviewMiniPlayerPosition {
6 readonly x: number;
7 readonly y: number;
8}
9​
10export interface PreviewMiniPlayerSize {
11 readonly width: number;
12 readonly height: number;
14​
15export type PreviewMiniPlayerSource =
16 | { readonly kind: "browser"; readonly tabId: string }
17 | {
18 readonly kind: "device";
19 readonly hostId: string;
20 readonly deviceId: string;
21 readonly platform: DevicePlatform;
22 readonly name: string;
23 };
24​
25export interface PreviewMiniPlayerState {
26 readonly source: PreviewMiniPlayerSource;
27 readonly position: PreviewMiniPlayerPosition | null;
28 readonly width: number | null;
29 readonly lastInteraction: "drag" | "resize";
31​
32interface PreviewMiniPlayerStoreState {
33 readonly byThreadKey: Record<string, PreviewMiniPlayerState>;
34 readonly open: (ref: ScopedThreadRef, source: PreviewMiniPlayerSource) => void;
35 readonly close: (ref: ScopedThreadRef) => void;
36 /** `sourceKey` guards against a drag that outlives the source it started on. */
37 readonly move: (
38 ref: ScopedThreadRef,
39 sourceKey: string,
40 position: PreviewMiniPlayerPosition,
41 ) => void;
42 readonly resize: (
43 ref: ScopedThreadRef,
44 sourceKey: string,
45 width: number,
46 position?: PreviewMiniPlayerPosition,
47 ) => void;
48 readonly removeThread: (ref: ScopedThreadRef) => void;
50​
51export function previewMiniPlayerSourceKey(source: PreviewMiniPlayerSource): string {
52 return source.kind === "browser"
53 ? `browser:${source.tabId}`
54 : `device:${encodeURIComponent(source.hostId)}:${encodeURIComponent(source.deviceId)}`;
56​
57export const browserMiniPlayerSource = (tabId: string): PreviewMiniPlayerSource => ({
58 kind: "browser",
59 tabId,
60});
61​
62export const usePreviewMiniPlayerStore = create<PreviewMiniPlayerStoreState>()((set) => ({
63 byThreadKey: {},
64 open: (ref, source) =>
65 set((state) => {
66 const threadKey = scopedThreadKey(ref);
67 const current = state.byThreadKey[threadKey];
68 if (
69 current &&
70 previewMiniPlayerSourceKey(current.source) === previewMiniPlayerSourceKey(source)
71 ) {
72 return state;
73 }
74 return {
75 byThreadKey: {
76 ...state.byThreadKey,
77 [threadKey]: {
78 source,
79 position: current?.position ?? null,
80 width: current?.width ?? null,
81 lastInteraction: current?.lastInteraction ?? "drag",
82 },
83 },
84 };
85 }),
86 close: (ref) =>
87 set((state) => {
88 const threadKey = scopedThreadKey(ref);
89 if (!(threadKey in state.byThreadKey)) return state;
90 const { [threadKey]: _closed, ...byThreadKey } = state.byThreadKey;
91 return { byThreadKey };
92 }),
93 move: (ref, sourceKey, position) =>
94 set((state) => {
95 const threadKey = scopedThreadKey(ref);
96 const current = state.byThreadKey[threadKey];
97 if (!current || previewMiniPlayerSourceKey(current.source) !== sourceKey) return state;
98 if (
99 current.position?.x === position.x &&
100 current.position.y === position.y &&
101 current.lastInteraction === "drag"
102 )
103 return state;
104 return {
105 byThreadKey: {
106 ...state.byThreadKey,
107 [threadKey]: { ...current, position, lastInteraction: "drag" },
108 },
109 };
110 }),
111 resize: (ref, sourceKey, width, position) =>
112 set((state) => {
113 const threadKey = scopedThreadKey(ref);
114 const current = state.byThreadKey[threadKey];
115 if (
116 !current ||
117 previewMiniPlayerSourceKey(current.source) !== sourceKey ||
118 (current.width === width &&
119 current.lastInteraction === "resize" &&
120 (!position || (current.position?.x === position.x && current.position.y === position.y)))
121 ) {
122 return state;
123 }
124 return {
125 byThreadKey: {
126 ...state.byThreadKey,
127 [threadKey]: {
128 ...current,
129 width,
130 position: position ?? current.position,
131 lastInteraction: "resize",
132 },
133 },
134 };
135 }),
136 removeThread: (ref) =>
137 set((state) => {
138 const threadKey = scopedThreadKey(ref);
139 if (!(threadKey in state.byThreadKey)) return state;
140 const { [threadKey]: _removed, ...byThreadKey } = state.byThreadKey;
141 return { byThreadKey };
142 }),
143}));
144​
145export function selectThreadPreviewMiniPlayer(
146 byThreadKey: Record<string, PreviewMiniPlayerState>,
147 ref: ScopedThreadRef | null | undefined,
148): PreviewMiniPlayerState | null {
149 if (!ref) return null;
150 return byThreadKey[scopedThreadKey(ref)] ?? null;
152​
153export function selectThreadPreviewMiniPlayerTabId(
154 byThreadKey: Record<string, PreviewMiniPlayerState>,
155 ref: ScopedThreadRef | null | undefined,
156): string | null {
157 const source = selectThreadPreviewMiniPlayer(byThreadKey, ref)?.source;
158 return source?.kind === "browser" ? source.tabId : null;