@shumin13/claude-pet 0.1.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.
Potentially problematic release.
This version of @shumin13/claude-pet might be problematic. Click here for more details.
- package/README.md +274 -0
- package/bin/claude-pet.js +85 -0
- package/hooks/claude-pet-clear.js +17 -0
- package/hooks/claude-pet-notify.js +26 -0
- package/hooks/claude-pet-stop.js +24 -0
- package/lib/config.js +19 -0
- package/lib/lock.js +35 -0
- package/lib/overlay-binary.js +104 -0
- package/lib/runtime.js +49 -0
- package/lib/session-labels.js +86 -0
- package/macos/RobotPetOverlay.swift +101 -0
- package/package.json +36 -0
- package/prebuilt/macos/robot-pet-overlay +0 -0
- package/public/app.js +516 -0
- package/public/desktop.css +719 -0
- package/public/index.html +103 -0
- package/public/styles.css +34 -0
- package/scripts/close-desktop-if-last-session.js +73 -0
- package/scripts/install-claude-hook.js +78 -0
- package/scripts/launch-desktop-if-needed.js +77 -0
- package/scripts/launch-desktop-if-needed.sh +7 -0
- package/scripts/run-desktop.sh +7 -0
- package/scripts/setup.js +198 -0
- package/server.js +139 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
const shell = document.querySelector(".pet-shell");
|
|
2
|
+
const speech = document.querySelector("#speech");
|
|
3
|
+
const eventType = document.querySelector("#eventType");
|
|
4
|
+
const eventTitle = document.querySelector("#eventTitle");
|
|
5
|
+
const eventMessage = document.querySelector("#eventMessage");
|
|
6
|
+
const collapsedBadge = document.querySelector("#collapsedBadge");
|
|
7
|
+
const collapseButton = document.querySelector(".collapse-button");
|
|
8
|
+
const resizeHandle = document.querySelector(".resize-handle");
|
|
9
|
+
const projectTray = document.querySelector("#projectTray");
|
|
10
|
+
|
|
11
|
+
document.documentElement.dataset.mode = window.location.pathname.endsWith("/desktop.html")
|
|
12
|
+
? "desktop"
|
|
13
|
+
: "preview";
|
|
14
|
+
|
|
15
|
+
const minScale = 0.86;
|
|
16
|
+
const maxScale = 1.58;
|
|
17
|
+
const defaultScale = 1;
|
|
18
|
+
const duplicateWindowMs = 4000;
|
|
19
|
+
const projectPrefixPattern = /^\[([^\]]+)\]/;
|
|
20
|
+
const projectMessagePrefixPattern = /^\[[^\]]+\]\s*/;
|
|
21
|
+
const eventPriority = {
|
|
22
|
+
idle_prompt: 1,
|
|
23
|
+
job_done: 2,
|
|
24
|
+
permission_prompt: 3
|
|
25
|
+
};
|
|
26
|
+
const recentEvents = new Map();
|
|
27
|
+
let currentEvent = null;
|
|
28
|
+
let queue = [];
|
|
29
|
+
let collapsed = false;
|
|
30
|
+
let currentScale = defaultScale;
|
|
31
|
+
|
|
32
|
+
const copy = {
|
|
33
|
+
permission_prompt: {
|
|
34
|
+
type: "permission_prompt",
|
|
35
|
+
label: "Permission",
|
|
36
|
+
title: "Claude needs a nod",
|
|
37
|
+
message: "A tool or command is waiting for your approval."
|
|
38
|
+
},
|
|
39
|
+
idle_prompt: {
|
|
40
|
+
type: "idle_prompt",
|
|
41
|
+
label: "Idle",
|
|
42
|
+
title: "Claude Pet is peeking",
|
|
43
|
+
message: "Claude has been waiting quietly for your next move."
|
|
44
|
+
},
|
|
45
|
+
job_done: {
|
|
46
|
+
type: "job_done",
|
|
47
|
+
label: "Done",
|
|
48
|
+
title: "Job done",
|
|
49
|
+
message: "Claude finished the current response."
|
|
50
|
+
},
|
|
51
|
+
auth_success: {
|
|
52
|
+
type: "auth_success",
|
|
53
|
+
label: "Success",
|
|
54
|
+
title: "Permission granted",
|
|
55
|
+
message: "Claude can continue now."
|
|
56
|
+
},
|
|
57
|
+
elicitation_dialog: {
|
|
58
|
+
type: "elicitation_dialog",
|
|
59
|
+
label: "Input",
|
|
60
|
+
title: "Claude has a question",
|
|
61
|
+
message: "A response is needed before work can continue."
|
|
62
|
+
},
|
|
63
|
+
elicitation_complete: {
|
|
64
|
+
type: "elicitation_complete",
|
|
65
|
+
label: "Done",
|
|
66
|
+
title: "Answer received",
|
|
67
|
+
message: "Claude has what it needs."
|
|
68
|
+
},
|
|
69
|
+
elicitation_response: {
|
|
70
|
+
type: "elicitation_response",
|
|
71
|
+
label: "Reply",
|
|
72
|
+
title: "Message sent",
|
|
73
|
+
message: "Your response reached Claude."
|
|
74
|
+
},
|
|
75
|
+
ready: {
|
|
76
|
+
type: "ready",
|
|
77
|
+
label: "Ready",
|
|
78
|
+
title: "Claude Pet is awake",
|
|
79
|
+
message: ""
|
|
80
|
+
},
|
|
81
|
+
notification: {
|
|
82
|
+
type: "notification",
|
|
83
|
+
label: "Notice",
|
|
84
|
+
title: "Claude Code",
|
|
85
|
+
message: "Claude Code needs your attention."
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function humanize(type) {
|
|
90
|
+
return String(type || "notification").replaceAll("_", " ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function priority(event) {
|
|
94
|
+
return eventPriority[event.type] ?? 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeEvent(event = {}) {
|
|
98
|
+
const type = event.type || event.notification_type || "notification";
|
|
99
|
+
const fallback = copy[type] || copy.notification;
|
|
100
|
+
return {
|
|
101
|
+
type,
|
|
102
|
+
label: fallback.label || humanize(type),
|
|
103
|
+
title: event.title || fallback.title,
|
|
104
|
+
message: event.message || fallback.message,
|
|
105
|
+
createdAt: event.createdAt || new Date().toISOString()
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function projectLabel(event = {}) {
|
|
110
|
+
return String(event.message || "").match(projectPrefixPattern)?.[1] || "Claude";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function messageWithoutProject(event = {}) {
|
|
114
|
+
return String(event.message || "").replace(projectMessagePrefixPattern, "");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function notificationItems() {
|
|
118
|
+
return [currentEvent, ...queue].filter(item => item && item.type !== "ready");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pendingCount() {
|
|
122
|
+
return notificationItems().length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function duplicateKey(event) {
|
|
126
|
+
const session = String(event.message || "").match(projectPrefixPattern)?.[1] || "";
|
|
127
|
+
return `${event.type}:${session}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isDuplicate(event) {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const key = duplicateKey(event);
|
|
133
|
+
for (const [seenKey, seenAt] of recentEvents) {
|
|
134
|
+
if (now - seenAt > duplicateWindowMs) recentEvents.delete(seenKey);
|
|
135
|
+
}
|
|
136
|
+
if (recentEvents.has(key) && now - recentEvents.get(key) < duplicateWindowMs) return true;
|
|
137
|
+
recentEvents.set(key, now);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function render(event = currentEvent) {
|
|
142
|
+
const item = normalizeEvent(event || copy.ready);
|
|
143
|
+
const pending = pendingCount();
|
|
144
|
+
const groups = projectGroups();
|
|
145
|
+
shell.dataset.state = item.type;
|
|
146
|
+
shell.dataset.collapsed = String(collapsed && item.type !== "ready");
|
|
147
|
+
shell.dataset.hasQueue = String(pending > 1);
|
|
148
|
+
shell.dataset.multiProject = String(!collapsed && groups.length > 1);
|
|
149
|
+
eventType.textContent = item.label;
|
|
150
|
+
eventTitle.textContent = item.title;
|
|
151
|
+
eventMessage.textContent = item.message;
|
|
152
|
+
|
|
153
|
+
if (collapsedBadge) {
|
|
154
|
+
collapsedBadge.textContent = String(Math.max(1, pending));
|
|
155
|
+
collapsedBadge.hidden = !(collapsed && pending > 0);
|
|
156
|
+
}
|
|
157
|
+
renderProjectTray(groups);
|
|
158
|
+
|
|
159
|
+
speech?.classList.remove("flash");
|
|
160
|
+
requestAnimationFrame(() => speech?.classList.add("flash"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderProjectTray(groups = projectGroups()) {
|
|
164
|
+
if (!projectTray) return;
|
|
165
|
+
projectTray.replaceChildren();
|
|
166
|
+
for (const group of groups) {
|
|
167
|
+
const bubble = document.createElement("button");
|
|
168
|
+
bubble.className = "project-bubble";
|
|
169
|
+
bubble.type = "button";
|
|
170
|
+
bubble.dataset.project = group.label;
|
|
171
|
+
bubble.dataset.urgent = String(group.urgent);
|
|
172
|
+
bubble.title = `Show ${group.label}`;
|
|
173
|
+
|
|
174
|
+
const head = document.createElement("div");
|
|
175
|
+
head.className = "project-bubble-head";
|
|
176
|
+
|
|
177
|
+
const label = document.createElement("span");
|
|
178
|
+
label.className = "project-bubble-label";
|
|
179
|
+
label.textContent = group.label;
|
|
180
|
+
head.append(label);
|
|
181
|
+
|
|
182
|
+
if (group.count > 1) {
|
|
183
|
+
const count = document.createElement("span");
|
|
184
|
+
count.className = "project-bubble-count";
|
|
185
|
+
count.textContent = String(group.count);
|
|
186
|
+
head.append(count);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const message = document.createElement("p");
|
|
190
|
+
message.className = "project-bubble-message";
|
|
191
|
+
message.textContent = messageWithoutProject(group.event);
|
|
192
|
+
|
|
193
|
+
bubble.append(head, message);
|
|
194
|
+
projectTray.append(bubble);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function projectGroups() {
|
|
199
|
+
const groups = new Map();
|
|
200
|
+
for (const event of notificationItems()) {
|
|
201
|
+
const label = projectLabel(event);
|
|
202
|
+
const group = groups.get(label) || {
|
|
203
|
+
label,
|
|
204
|
+
count: 0,
|
|
205
|
+
event,
|
|
206
|
+
urgent: false,
|
|
207
|
+
rank: priority(event)
|
|
208
|
+
};
|
|
209
|
+
group.count += 1;
|
|
210
|
+
group.urgent ||= event.type === "permission_prompt";
|
|
211
|
+
if (priority(event) > group.rank) {
|
|
212
|
+
group.event = event;
|
|
213
|
+
group.rank = priority(event);
|
|
214
|
+
}
|
|
215
|
+
groups.set(label, group);
|
|
216
|
+
}
|
|
217
|
+
return [...groups.values()].sort((a, b) => b.rank - a.rank);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function selectProject(label) {
|
|
221
|
+
const items = notificationItems();
|
|
222
|
+
const selected = items.find(event => projectLabel(event) === label);
|
|
223
|
+
if (!selected) return;
|
|
224
|
+
currentEvent = selected;
|
|
225
|
+
queue = items.filter(event => event !== selected);
|
|
226
|
+
expandNotifications();
|
|
227
|
+
render();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function popNext() {
|
|
231
|
+
queue.sort((a, b) => priority(b) - priority(a));
|
|
232
|
+
currentEvent = queue.shift() || normalizeEvent(copy.ready);
|
|
233
|
+
expandNotifications();
|
|
234
|
+
render();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function showEvent(event) {
|
|
238
|
+
currentEvent = event;
|
|
239
|
+
expandNotifications();
|
|
240
|
+
render();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function overrideEvent(rawEvent) {
|
|
244
|
+
currentEvent = normalizeEvent(rawEvent);
|
|
245
|
+
queue = [];
|
|
246
|
+
expandNotifications();
|
|
247
|
+
render();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function expandNotifications() {
|
|
251
|
+
collapsed = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applyEvent(rawEvent) {
|
|
255
|
+
const event = normalizeEvent(rawEvent);
|
|
256
|
+
if (event.type === "ready") {
|
|
257
|
+
if (queue.length > 0) popNext();
|
|
258
|
+
else showEvent(event);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (isDuplicate(event)) return;
|
|
263
|
+
|
|
264
|
+
if (!currentEvent || currentEvent.type === "ready") {
|
|
265
|
+
showEvent(event);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (priority(event) > priority(currentEvent)) {
|
|
270
|
+
queue.push(currentEvent);
|
|
271
|
+
currentEvent = event;
|
|
272
|
+
expandNotifications();
|
|
273
|
+
} else {
|
|
274
|
+
queue.push(event);
|
|
275
|
+
}
|
|
276
|
+
render();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function connectStream() {
|
|
280
|
+
if (window.location.protocol === "file:" || new URLSearchParams(window.location.search).has("demo")) {
|
|
281
|
+
applyDemoEventsFromUrl();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const source = new EventSource("/events/stream");
|
|
286
|
+
source.onmessage = message => {
|
|
287
|
+
shell.dataset.offline = "false";
|
|
288
|
+
try {
|
|
289
|
+
applyEvent(JSON.parse(message.data));
|
|
290
|
+
} catch {
|
|
291
|
+
applyEvent(copy.notification);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
source.onerror = () => {
|
|
295
|
+
shell.dataset.offline = "true";
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function applyDemoEventsFromUrl() {
|
|
300
|
+
const params = new URLSearchParams(window.location.search);
|
|
301
|
+
const demo = params.get("demo") || "ready";
|
|
302
|
+
const demoEvents = {
|
|
303
|
+
ready: [copy.ready],
|
|
304
|
+
permission: [{
|
|
305
|
+
type: "permission_prompt",
|
|
306
|
+
title: "Claude needs a nod",
|
|
307
|
+
message: "[Website] Claude wants to run a command."
|
|
308
|
+
}],
|
|
309
|
+
idle: [{
|
|
310
|
+
type: "idle_prompt",
|
|
311
|
+
title: "Claude Pet is peeking",
|
|
312
|
+
message: "[Docs] Claude is waiting for your next instruction."
|
|
313
|
+
}],
|
|
314
|
+
done: [{
|
|
315
|
+
type: "job_done",
|
|
316
|
+
title: "Job done",
|
|
317
|
+
message: "[Claude Pet] Claude finished the current response."
|
|
318
|
+
}],
|
|
319
|
+
one: [{
|
|
320
|
+
type: "notification",
|
|
321
|
+
title: "Claude Code",
|
|
322
|
+
message: "[Claude Pet] One notification is waiting."
|
|
323
|
+
}],
|
|
324
|
+
multi: [
|
|
325
|
+
{
|
|
326
|
+
type: "permission_prompt",
|
|
327
|
+
title: "Permission needed",
|
|
328
|
+
message: "[Website] Claude wants to run a command."
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: "idle_prompt",
|
|
332
|
+
title: "Still here",
|
|
333
|
+
message: "[Docs] Claude is waiting quietly."
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
type: "job_done",
|
|
337
|
+
title: "Job done",
|
|
338
|
+
message: "[Claude Pet] Claude finished the current response."
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
for (const event of demoEvents[demo] || demoEvents.ready) applyEvent(event);
|
|
344
|
+
if (params.get("collapsed") === "true") {
|
|
345
|
+
collapsed = true;
|
|
346
|
+
render();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function clampScale(scale) {
|
|
351
|
+
return Math.min(maxScale, Math.max(minScale, scale));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sizeName(scale) {
|
|
355
|
+
if (scale < 1.08) return "small";
|
|
356
|
+
if (scale < 1.34) return "medium";
|
|
357
|
+
return "large";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function setScale(scale, persist = true) {
|
|
361
|
+
currentScale = clampScale(scale);
|
|
362
|
+
document.documentElement.style.setProperty("--pet-scale", String(currentScale));
|
|
363
|
+
shell.dataset.size = sizeName(currentScale);
|
|
364
|
+
try {
|
|
365
|
+
if (persist) localStorage.setItem("claude-pet-scale", String(currentScale));
|
|
366
|
+
} catch {
|
|
367
|
+
// Static previews can run without storage.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const button of document.querySelectorAll("[data-demo]")) {
|
|
372
|
+
button.addEventListener("click", () => {
|
|
373
|
+
const type = button.dataset.demo;
|
|
374
|
+
overrideEvent({ type, ...copy[type] });
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
projectTray?.addEventListener("click", event => {
|
|
379
|
+
const bubble = event.target.closest?.("[data-project]");
|
|
380
|
+
if (!bubble) return;
|
|
381
|
+
event.stopPropagation();
|
|
382
|
+
selectProject(bubble.dataset.project);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
collapseButton?.addEventListener("click", event => {
|
|
386
|
+
event.stopPropagation();
|
|
387
|
+
if (!currentEvent || currentEvent.type === "ready") return;
|
|
388
|
+
collapsed = true;
|
|
389
|
+
render();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
collapsedBadge?.addEventListener("click", event => {
|
|
393
|
+
event.stopPropagation();
|
|
394
|
+
collapsed = false;
|
|
395
|
+
render();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
shell?.addEventListener("click", () => {
|
|
399
|
+
if (collapsed) {
|
|
400
|
+
collapsed = false;
|
|
401
|
+
render();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const savedScale = Number(localStorage.getItem("claude-pet-scale"));
|
|
407
|
+
setScale(Number.isFinite(savedScale) ? savedScale : defaultScale, false);
|
|
408
|
+
} catch {
|
|
409
|
+
setScale(defaultScale, false);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const previewScale = Number(new URLSearchParams(window.location.search).get("scale"));
|
|
413
|
+
if (Number.isFinite(previewScale) && previewScale > 0) setScale(previewScale, false);
|
|
414
|
+
|
|
415
|
+
if (new URLSearchParams(window.location.search).get("snapshot") === "true") {
|
|
416
|
+
shell.dataset.snapshot = "true";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (new URLSearchParams(window.location.search).get("demoMotion") === "true") {
|
|
420
|
+
shell.dataset.demoMotion = "true";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
connectStream();
|
|
424
|
+
|
|
425
|
+
function connectResize() {
|
|
426
|
+
let resizing = false;
|
|
427
|
+
let resizeStartX = 0;
|
|
428
|
+
let resizeStartY = 0;
|
|
429
|
+
let resizeStartScale = currentScale;
|
|
430
|
+
let resizeFrame = 0;
|
|
431
|
+
|
|
432
|
+
const applyResize = event => {
|
|
433
|
+
const delta = Math.max(event.clientX - resizeStartX, event.clientY - resizeStartY);
|
|
434
|
+
const nextScale = resizeStartScale + delta / 180;
|
|
435
|
+
if (resizeFrame) cancelAnimationFrame(resizeFrame);
|
|
436
|
+
resizeFrame = requestAnimationFrame(() => {
|
|
437
|
+
resizeFrame = 0;
|
|
438
|
+
setScale(nextScale, false);
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
resizeHandle?.addEventListener("pointerdown", event => {
|
|
443
|
+
resizing = true;
|
|
444
|
+
resizeStartX = event.clientX;
|
|
445
|
+
resizeStartY = event.clientY;
|
|
446
|
+
resizeStartScale = currentScale;
|
|
447
|
+
shell.dataset.resizing = "true";
|
|
448
|
+
resizeHandle.setPointerCapture?.(event.pointerId);
|
|
449
|
+
event.stopPropagation();
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
resizeHandle?.addEventListener("pointermove", event => {
|
|
454
|
+
if (!resizing) return;
|
|
455
|
+
applyResize(event);
|
|
456
|
+
event.stopPropagation();
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
document.addEventListener("pointermove", event => {
|
|
461
|
+
if (!resizing) return;
|
|
462
|
+
applyResize(event);
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const endResize = event => {
|
|
467
|
+
if (!resizing) return;
|
|
468
|
+
resizing = false;
|
|
469
|
+
shell.dataset.resizing = "false";
|
|
470
|
+
resizeHandle.releasePointerCapture?.(event.pointerId);
|
|
471
|
+
setScale(currentScale);
|
|
472
|
+
event.stopPropagation();
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
resizeHandle?.addEventListener("pointerup", endResize);
|
|
476
|
+
resizeHandle?.addEventListener("pointercancel", endResize);
|
|
477
|
+
document.addEventListener("pointerup", endResize);
|
|
478
|
+
document.addEventListener("pointercancel", endResize);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function connectDesktopDrag() {
|
|
482
|
+
const bridge = window.webkit?.messageHandlers?.petDrag;
|
|
483
|
+
if (!bridge) return;
|
|
484
|
+
const closeButton = document.querySelector(".close-button");
|
|
485
|
+
|
|
486
|
+
let dragging = false;
|
|
487
|
+
|
|
488
|
+
const start = event => {
|
|
489
|
+
if (event.target.closest?.("button")) return;
|
|
490
|
+
dragging = true;
|
|
491
|
+
bridge.postMessage({ type: "start" });
|
|
492
|
+
event.preventDefault();
|
|
493
|
+
};
|
|
494
|
+
const move = event => {
|
|
495
|
+
if (!dragging) return;
|
|
496
|
+
bridge.postMessage({ type: "move" });
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
};
|
|
499
|
+
const end = () => {
|
|
500
|
+
if (!dragging) return;
|
|
501
|
+
dragging = false;
|
|
502
|
+
bridge.postMessage({ type: "end" });
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
document.addEventListener("pointerdown", start);
|
|
506
|
+
document.addEventListener("pointermove", move);
|
|
507
|
+
document.addEventListener("pointerup", end);
|
|
508
|
+
document.addEventListener("pointercancel", end);
|
|
509
|
+
closeButton?.addEventListener("click", event => {
|
|
510
|
+
event.stopPropagation();
|
|
511
|
+
bridge.postMessage({ type: "close" });
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
connectResize();
|
|
516
|
+
connectDesktopDrag();
|