palmier 0.7.9 → 0.8.1
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/commands/run.js +55 -0
- package/dist/commands/serve.js +22 -2
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +36 -0
- package/dist/event-queues.js +53 -0
- package/dist/mcp-tools.js +2 -2
- package/dist/platform/windows.js +5 -2
- package/dist/pwa/assets/index-CQxcuDhM.css +1 -0
- package/dist/pwa/assets/index-DQfOEB03.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-D7Kq3Nvk.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-DOyOiwsW.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +6 -5
- package/dist/transports/http-transport.js +15 -0
- package/dist/types.d.ts +6 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +5 -0
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +155 -456
- package/palmier-server/pwa/src/components/SessionsView.tsx +9 -3
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +79 -32
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +48 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/spec.md +12 -2
- package/src/commands/run.ts +61 -0
- package/src/commands/serve.ts +22 -2
- package/src/device-capabilities.ts +1 -1
- package/src/event-queues.ts +56 -0
- package/src/mcp-tools.ts +2 -2
- package/src/platform/windows.ts +5 -2
- package/src/rpc-handler.ts +8 -7
- package/src/transports/http-transport.ts +14 -0
- package/src/types.ts +6 -5
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
|
@@ -1,101 +1,9 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { useNavigate } from "react-router-dom";
|
|
4
|
-
import { Capacitor
|
|
5
|
-
import { App as
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
interface LocationPermissionResult {
|
|
9
|
-
fine: boolean;
|
|
10
|
-
background: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface LocationPermissionPlugin {
|
|
14
|
-
request(): Promise<LocationPermissionResult>;
|
|
15
|
-
check(): Promise<LocationPermissionResult>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface NotificationListenerResult {
|
|
19
|
-
enabled: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface NotificationListenerPlugin {
|
|
23
|
-
request(): Promise<NotificationListenerResult>;
|
|
24
|
-
check(): Promise<NotificationListenerResult>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const LocationPermission = Capacitor.isNativePlatform()
|
|
28
|
-
? registerPlugin<LocationPermissionPlugin>("LocationPermission")
|
|
29
|
-
: null;
|
|
30
|
-
|
|
31
|
-
interface SmsPermissionResult {
|
|
32
|
-
granted: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface SmsPermissionPlugin {
|
|
36
|
-
request(): Promise<SmsPermissionResult>;
|
|
37
|
-
check(): Promise<SmsPermissionResult>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ContactsPermissionResult {
|
|
41
|
-
granted: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface ContactsPermissionPlugin {
|
|
45
|
-
request(): Promise<ContactsPermissionResult>;
|
|
46
|
-
check(): Promise<ContactsPermissionResult>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface CalendarPermissionResult {
|
|
50
|
-
granted: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface CalendarPermissionPlugin {
|
|
54
|
-
request(): Promise<CalendarPermissionResult>;
|
|
55
|
-
check(): Promise<CalendarPermissionResult>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface DndAccessResult {
|
|
59
|
-
enabled: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface DndAccessPlugin {
|
|
63
|
-
request(): Promise<DndAccessResult>;
|
|
64
|
-
check(): Promise<DndAccessResult>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface FullScreenIntentResult {
|
|
68
|
-
granted: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface FullScreenIntentPlugin {
|
|
72
|
-
request(): Promise<FullScreenIntentResult>;
|
|
73
|
-
check(): Promise<FullScreenIntentResult>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const NotificationListener = Capacitor.isNativePlatform()
|
|
77
|
-
? registerPlugin<NotificationListenerPlugin>("NotificationListener")
|
|
78
|
-
: null;
|
|
79
|
-
|
|
80
|
-
const SmsPermission = Capacitor.isNativePlatform()
|
|
81
|
-
? registerPlugin<SmsPermissionPlugin>("SmsPermission")
|
|
82
|
-
: null;
|
|
83
|
-
|
|
84
|
-
const ContactsPermission = Capacitor.isNativePlatform()
|
|
85
|
-
? registerPlugin<ContactsPermissionPlugin>("ContactsPermission")
|
|
86
|
-
: null;
|
|
87
|
-
|
|
88
|
-
const CalendarPermission = Capacitor.isNativePlatform()
|
|
89
|
-
? registerPlugin<CalendarPermissionPlugin>("CalendarPermission")
|
|
90
|
-
: null;
|
|
91
|
-
|
|
92
|
-
const DndAccess = Capacitor.isNativePlatform()
|
|
93
|
-
? registerPlugin<DndAccessPlugin>("DndAccess")
|
|
94
|
-
: null;
|
|
95
|
-
|
|
96
|
-
const FullScreenIntent = Capacitor.isNativePlatform()
|
|
97
|
-
? registerPlugin<FullScreenIntentPlugin>("FullScreenIntent")
|
|
98
|
-
: null;
|
|
4
|
+
import { Capacitor } from "@capacitor/core";
|
|
5
|
+
import { App as CapacitorApp } from "@capacitor/app";
|
|
6
|
+
import { Device, type PermissionType } from "../native/Device";
|
|
99
7
|
import { useHostStore } from "../contexts/HostStoreContext";
|
|
100
8
|
import { useMediaQuery } from "../hooks/useMediaQuery";
|
|
101
9
|
import { confirmLeaveDraft } from "../draftGuard";
|
|
@@ -104,6 +12,42 @@ import { confirmLeaveDraft } from "../draftGuard";
|
|
|
104
12
|
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
105
13
|
const isNative = Capacitor.isNativePlatform();
|
|
106
14
|
|
|
15
|
+
interface CapabilityDefinition {
|
|
16
|
+
/** Server-side capability name used in device.capability.{enable,disable} RPCs. */
|
|
17
|
+
capability: string;
|
|
18
|
+
/** Label shown in the drawer toggle. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Runtime or settings permission to request before enabling. */
|
|
21
|
+
permission?: PermissionType;
|
|
22
|
+
/** True for capabilities that display full-screen alerts (alert, send-email). */
|
|
23
|
+
needsFullScreenIntent?: boolean;
|
|
24
|
+
/** Override RPC methods; location uses device.location.{enable,disable} instead. */
|
|
25
|
+
enableMethod?: string;
|
|
26
|
+
disableMethod?: string;
|
|
27
|
+
enableParams?(fcmToken: string): Record<string, unknown>;
|
|
28
|
+
disableParams?(): Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CAPABILITIES: CapabilityDefinition[] = [
|
|
32
|
+
{
|
|
33
|
+
capability: "location",
|
|
34
|
+
label: "Location Access",
|
|
35
|
+
permission: "location",
|
|
36
|
+
enableMethod: "device.location.enable",
|
|
37
|
+
disableMethod: "device.location.disable",
|
|
38
|
+
enableParams: (fcmToken) => ({ fcmToken }),
|
|
39
|
+
disableParams: () => ({}),
|
|
40
|
+
},
|
|
41
|
+
{ capability: "notifications", label: "Notification Access", permission: "notificationListener" },
|
|
42
|
+
{ capability: "sms", label: "SMS Access", permission: "sms" },
|
|
43
|
+
{ capability: "contacts", label: "Contacts Access", permission: "contacts" },
|
|
44
|
+
{ capability: "calendar", label: "Calendar Access", permission: "calendar" },
|
|
45
|
+
{ capability: "dnd", label: "Do Not Disturb Control", permission: "dnd" },
|
|
46
|
+
{ capability: "alert", label: "Alert Access", needsFullScreenIntent: true },
|
|
47
|
+
{ capability: "battery", label: "Battery Access" },
|
|
48
|
+
{ capability: "send-email", label: "Email Drafting", needsFullScreenIntent: true },
|
|
49
|
+
];
|
|
50
|
+
|
|
107
51
|
interface HostMenuProps {
|
|
108
52
|
daemonVersion?: string | null;
|
|
109
53
|
capabilityTokens?: Record<string, string | null>;
|
|
@@ -122,294 +66,140 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
122
66
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
123
67
|
const [renameValue, setRenameValue] = useState("");
|
|
124
68
|
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
|
|
125
|
-
const [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const [
|
|
133
|
-
|
|
69
|
+
const [togglingCapability, setTogglingCapability] = useState<string | null>(null);
|
|
70
|
+
/**
|
|
71
|
+
* Permission types the installed APK understands. Null while loading; an empty
|
|
72
|
+
* set means the native plugin doesn't expose a discovery method (pre-Device
|
|
73
|
+
* plugin build) — in that case we don't pre-filter the UI and rely on per-call
|
|
74
|
+
* {supported: false} from the native side as the fallback.
|
|
75
|
+
*/
|
|
76
|
+
const [supportedPerms, setSupportedPerms] = useState<Set<PermissionType> | null>(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!isNative || !Device) {
|
|
80
|
+
setSupportedPerms(new Set());
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
Device.getSupportedPermissions()
|
|
84
|
+
.then(({ types }) => setSupportedPerms(new Set(types)))
|
|
85
|
+
.catch(() => setSupportedPerms(new Set())); // old APK: fall back to per-call supported flag
|
|
86
|
+
}, []);
|
|
134
87
|
|
|
135
88
|
// Capability enabled = this device's client token matches the registered device for that capability
|
|
136
|
-
function
|
|
137
|
-
return !!(activeClientToken && capabilityTokens?.[
|
|
89
|
+
function isCapabilityEnabled(capability: string): boolean {
|
|
90
|
+
return !!(activeClientToken && capabilityTokens?.[capability] === activeClientToken);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A capability is shown when native either explicitly supports its permission, or
|
|
95
|
+
* can't advertise support (empty set = old APK or web) — in the latter case the
|
|
96
|
+
* toggle still works because the per-call `supported` flag guards at tap time.
|
|
97
|
+
*/
|
|
98
|
+
function isCapabilityVisible(definition: CapabilityDefinition): boolean {
|
|
99
|
+
if (!supportedPerms) return false;
|
|
100
|
+
if (supportedPerms.size === 0) return true;
|
|
101
|
+
if (definition.permission && !supportedPerms.has(definition.permission)) return false;
|
|
102
|
+
if (definition.needsFullScreenIntent && !supportedPerms.has("fullScreenIntent")) return false;
|
|
103
|
+
return true;
|
|
138
104
|
}
|
|
139
|
-
const locationEnabled = isCapEnabled("location");
|
|
140
|
-
const notificationListenerEnabled = isCapEnabled("notifications");
|
|
141
|
-
const smsEnabled = isCapEnabled("sms");
|
|
142
|
-
const contactsEnabled = isCapEnabled("contacts");
|
|
143
|
-
const calendarEnabled = isCapEnabled("calendar");
|
|
144
|
-
const dndEnabled = isCapEnabled("dnd");
|
|
145
|
-
const alarmEnabled = isCapEnabled("alert");
|
|
146
|
-
const batteryEnabled = isCapEnabled("battery");
|
|
147
|
-
const emailEnabled = isCapEnabled("email");
|
|
148
105
|
|
|
149
106
|
/** Update local capability tokens state after a toggle change */
|
|
150
|
-
function
|
|
107
|
+
function setCapabilityEnabled(capability: string, enabled: boolean) {
|
|
151
108
|
const updated: Record<string, string | null> = {};
|
|
152
109
|
for (const [k, v] of Object.entries(capabilityTokens ?? {})) updated[k] = v ?? null;
|
|
153
|
-
updated[
|
|
110
|
+
updated[capability] = enabled ? (activeClientToken ?? null) : null;
|
|
154
111
|
onCapabilityTokensChange?.(updated);
|
|
155
112
|
}
|
|
156
113
|
|
|
157
|
-
//
|
|
114
|
+
// If the OS location permission is revoked while the app is backgrounded,
|
|
115
|
+
// disable the capability on the host so agents don't keep pinging for fixes.
|
|
158
116
|
useEffect(() => {
|
|
159
|
-
if (!isNative || !
|
|
117
|
+
if (!isNative || !Device || !request) return;
|
|
118
|
+
|
|
119
|
+
const locationEnabled = isCapabilityEnabled("location");
|
|
120
|
+
if (!locationEnabled) return;
|
|
160
121
|
|
|
161
122
|
function syncPermissionState() {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!fine) {
|
|
165
|
-
// Permission revoked — disable on host
|
|
123
|
+
Device!.checkPermission({ type: "location" }).then(({ granted }) => {
|
|
124
|
+
if (!granted) {
|
|
166
125
|
request!("device.location.disable").then(() => {
|
|
167
|
-
|
|
126
|
+
setCapabilityEnabled("location", false);
|
|
168
127
|
}).catch(() => {});
|
|
169
128
|
}
|
|
170
129
|
});
|
|
171
130
|
}
|
|
172
131
|
|
|
173
132
|
syncPermissionState();
|
|
174
|
-
|
|
175
|
-
const listener = CapApp.addListener("resume", () => {
|
|
176
|
-
syncPermissionState();
|
|
177
|
-
});
|
|
178
|
-
|
|
133
|
+
const listener = CapacitorApp.addListener("resume", syncPermissionState);
|
|
179
134
|
return () => { listener.then((h) => h.remove()); };
|
|
180
|
-
}, [
|
|
135
|
+
}, [capabilityTokens, activeClientToken]);
|
|
181
136
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
137
|
+
// Mirror the server-derived enabled set into native as the local kill-switch.
|
|
138
|
+
// Toggling below writes through immediately; this useEffect catches host-initiated
|
|
139
|
+
// changes (e.g. a capability revoked on another device) on the next render.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!isNative || !Device) return;
|
|
142
|
+
const enabledCapabilities = CAPABILITIES
|
|
143
|
+
.map((definition) => definition.capability)
|
|
144
|
+
.filter((capability) => capabilityTokens?.[capability] === activeClientToken);
|
|
145
|
+
Device.setEnabledCapabilities({ capabilities: enabledCapabilities }).catch(() => {});
|
|
146
|
+
}, [capabilityTokens, activeClientToken]);
|
|
147
|
+
|
|
148
|
+
async function toggleCapability(definition: CapabilityDefinition) {
|
|
149
|
+
if (!request) return;
|
|
150
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
151
|
+
setTogglingCapability(definition.capability);
|
|
185
152
|
try {
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!systemEnabled) {
|
|
193
|
-
const result = await NotificationListener.request();
|
|
194
|
-
if (!result.enabled) return;
|
|
195
|
-
}
|
|
196
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
197
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
198
|
-
await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
|
|
199
|
-
await request("device.capability.enable", { capability: "notifications", fcmToken });
|
|
200
|
-
setCapEnabled("notifications", true);
|
|
153
|
+
if (enabled) {
|
|
154
|
+
const method = definition.disableMethod ?? "device.capability.disable";
|
|
155
|
+
const params = definition.disableParams?.() ?? { capability: definition.capability };
|
|
156
|
+
await request(method, params);
|
|
157
|
+
setCapabilityEnabled(definition.capability, false);
|
|
158
|
+
return;
|
|
201
159
|
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.error("Failed to toggle notification listener:", err);
|
|
204
|
-
} finally {
|
|
205
|
-
setTogglingNotificationListener(false);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
160
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await Preferences.set({ key: "smsListenerEnabled", value: "false" });
|
|
215
|
-
await request("device.capability.disable", { capability: "sms" });
|
|
216
|
-
setCapEnabled("sms", false);
|
|
217
|
-
} else {
|
|
218
|
-
const { granted } = await SmsPermission.check();
|
|
219
|
-
if (!granted) {
|
|
220
|
-
const result = await SmsPermission.request();
|
|
221
|
-
if (!result.granted) return;
|
|
161
|
+
if (Device && definition.permission) {
|
|
162
|
+
const check = await Device.checkPermission({ type: definition.permission });
|
|
163
|
+
if (!check.supported) {
|
|
164
|
+
console.warn(`Native build does not support permission '${definition.permission}'`);
|
|
165
|
+
return;
|
|
222
166
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await Preferences.set({ key: "smsListenerEnabled", value: "true" });
|
|
226
|
-
await request("device.capability.enable", { capability: "sms", fcmToken });
|
|
227
|
-
setCapEnabled("sms", true);
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.error("Failed to toggle SMS access:", err);
|
|
231
|
-
} finally {
|
|
232
|
-
setTogglingSms(false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async function handleContactsToggle() {
|
|
237
|
-
if (!ContactsPermission || !request) return;
|
|
238
|
-
setTogglingContacts(true);
|
|
239
|
-
try {
|
|
240
|
-
if (contactsEnabled) {
|
|
241
|
-
await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
|
|
242
|
-
await request("device.capability.disable", { capability: "contacts" });
|
|
243
|
-
setCapEnabled("contacts", false);
|
|
244
|
-
} else {
|
|
245
|
-
const { granted } = await ContactsPermission.check();
|
|
246
|
-
if (!granted) {
|
|
247
|
-
const result = await ContactsPermission.request();
|
|
167
|
+
if (!check.granted) {
|
|
168
|
+
const result = await Device.requestPermission({ type: definition.permission });
|
|
248
169
|
if (!result.granted) return;
|
|
249
170
|
}
|
|
250
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
251
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
252
|
-
await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
|
|
253
|
-
await request("device.capability.enable", { capability: "contacts", fcmToken });
|
|
254
|
-
setCapEnabled("contacts", true);
|
|
255
171
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function handleCalendarToggle() {
|
|
264
|
-
if (!CalendarPermission || !request) return;
|
|
265
|
-
setTogglingCalendar(true);
|
|
266
|
-
try {
|
|
267
|
-
if (calendarEnabled) {
|
|
268
|
-
await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
|
|
269
|
-
await request("device.capability.disable", { capability: "calendar" });
|
|
270
|
-
setCapEnabled("calendar", false);
|
|
271
|
-
} else {
|
|
272
|
-
const { granted } = await CalendarPermission.check();
|
|
273
|
-
if (!granted) {
|
|
274
|
-
const result = await CalendarPermission.request();
|
|
275
|
-
if (!result.granted) return;
|
|
172
|
+
if (Device && definition.needsFullScreenIntent) {
|
|
173
|
+
const check = await Device.checkPermission({ type: "fullScreenIntent" });
|
|
174
|
+
if (!check.supported) {
|
|
175
|
+
console.warn("Native build does not support fullScreenIntent");
|
|
176
|
+
return;
|
|
276
177
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
await request("device.capability.enable", { capability: "calendar", fcmToken });
|
|
281
|
-
setCapEnabled("calendar", true);
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error("Failed to toggle calendar access:", err);
|
|
285
|
-
} finally {
|
|
286
|
-
setTogglingCalendar(false);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function handleDndToggle() {
|
|
291
|
-
if (!DndAccess || !request) return;
|
|
292
|
-
setTogglingDnd(true);
|
|
293
|
-
try {
|
|
294
|
-
if (dndEnabled) {
|
|
295
|
-
await request("device.capability.disable", { capability: "dnd" });
|
|
296
|
-
setCapEnabled("dnd", false);
|
|
297
|
-
} else {
|
|
298
|
-
const { enabled: systemEnabled } = await DndAccess.check();
|
|
299
|
-
if (!systemEnabled) {
|
|
300
|
-
const result = await DndAccess.request();
|
|
301
|
-
if (!result.enabled) return;
|
|
178
|
+
if (!check.granted) {
|
|
179
|
+
const result = await Device.requestPermission({ type: "fullScreenIntent" });
|
|
180
|
+
if (!result.granted) return;
|
|
302
181
|
}
|
|
303
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
304
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
305
|
-
await request("device.capability.enable", { capability: "dnd", fcmToken });
|
|
306
|
-
setCapEnabled("dnd", true);
|
|
307
|
-
}
|
|
308
|
-
} catch (err) {
|
|
309
|
-
console.error("Failed to toggle DND access:", err);
|
|
310
|
-
} finally {
|
|
311
|
-
setTogglingDnd(false);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** Ensure full-screen intent permission is granted (needed for alert + email). */
|
|
316
|
-
async function ensureFullScreenIntent(): Promise<boolean> {
|
|
317
|
-
if (!FullScreenIntent) return true;
|
|
318
|
-
const { granted } = await FullScreenIntent.check();
|
|
319
|
-
if (granted) return true;
|
|
320
|
-
const result = await FullScreenIntent.request();
|
|
321
|
-
return result.granted;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function handleAlarmToggle() {
|
|
325
|
-
if (!request) return;
|
|
326
|
-
setTogglingAlarm(true);
|
|
327
|
-
try {
|
|
328
|
-
if (alarmEnabled) {
|
|
329
|
-
await request("device.capability.disable", { capability: "alert" });
|
|
330
|
-
setCapEnabled("alert", false);
|
|
331
|
-
} else {
|
|
332
|
-
if (!await ensureFullScreenIntent()) return;
|
|
333
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
334
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
335
|
-
await request("device.capability.enable", { capability: "alert", fcmToken });
|
|
336
|
-
setCapEnabled("alert", true);
|
|
337
|
-
}
|
|
338
|
-
} catch (err) {
|
|
339
|
-
console.error("Failed to toggle alert access:", err);
|
|
340
|
-
} finally {
|
|
341
|
-
setTogglingAlarm(false);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function handleEmailToggle() {
|
|
346
|
-
if (!request) return;
|
|
347
|
-
setTogglingEmail(true);
|
|
348
|
-
try {
|
|
349
|
-
if (emailEnabled) {
|
|
350
|
-
await request("device.capability.disable", { capability: "email" });
|
|
351
|
-
setCapEnabled("email", false);
|
|
352
|
-
} else {
|
|
353
|
-
if (!await ensureFullScreenIntent()) return;
|
|
354
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
355
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
356
|
-
await request("device.capability.enable", { capability: "email", fcmToken });
|
|
357
|
-
setCapEnabled("email", true);
|
|
358
|
-
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
console.error("Failed to toggle email access:", err);
|
|
361
|
-
} finally {
|
|
362
|
-
setTogglingEmail(false);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async function handleBatteryToggle() {
|
|
367
|
-
if (!request) return;
|
|
368
|
-
setTogglingBattery(true);
|
|
369
|
-
try {
|
|
370
|
-
if (batteryEnabled) {
|
|
371
|
-
await request("device.capability.disable", { capability: "battery" });
|
|
372
|
-
setCapEnabled("battery", false);
|
|
373
|
-
} else {
|
|
374
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
375
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
376
|
-
await request("device.capability.enable", { capability: "battery", fcmToken });
|
|
377
|
-
setCapEnabled("battery", true);
|
|
378
182
|
}
|
|
379
|
-
} catch (err) {
|
|
380
|
-
console.error("Failed to toggle battery access:", err);
|
|
381
|
-
} finally {
|
|
382
|
-
setTogglingBattery(false);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
183
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
402
|
-
if (!fcmToken) {
|
|
403
|
-
console.warn("No FCM token available");
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
await request("device.location.enable", { fcmToken });
|
|
407
|
-
setCapEnabled("location", true);
|
|
408
|
-
}
|
|
184
|
+
if (!Device) return;
|
|
185
|
+
const { token: fcmToken } = await Device.getFcmToken();
|
|
186
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
187
|
+
|
|
188
|
+
// Whitelist the capability natively before enabling on the host, so an FCM
|
|
189
|
+
// from the host can't arrive in the gap where our useEffect hasn't synced yet.
|
|
190
|
+
const enabledNow = CAPABILITIES
|
|
191
|
+
.map((c) => c.capability)
|
|
192
|
+
.filter((name) => name === definition.capability || capabilityTokens?.[name] === activeClientToken);
|
|
193
|
+
await Device.setEnabledCapabilities({ capabilities: enabledNow });
|
|
194
|
+
|
|
195
|
+
const method = definition.enableMethod ?? "device.capability.enable";
|
|
196
|
+
const params = definition.enableParams?.(fcmToken) ?? { capability: definition.capability, fcmToken };
|
|
197
|
+
await request(method, params);
|
|
198
|
+
setCapabilityEnabled(definition.capability, true);
|
|
409
199
|
} catch (err) {
|
|
410
|
-
console.error(
|
|
200
|
+
console.error(`Failed to toggle ${definition.capability}:`, err);
|
|
411
201
|
} finally {
|
|
412
|
-
|
|
202
|
+
setTogglingCapability(null);
|
|
413
203
|
}
|
|
414
204
|
}
|
|
415
205
|
const drawerRef = useRef<HTMLDivElement>(null);
|
|
@@ -596,114 +386,23 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
596
386
|
<>
|
|
597
387
|
<div className="drawer-divider" />
|
|
598
388
|
<div className="drawer-section">
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
disabled={togglingNotificationListener}
|
|
617
|
-
role="switch"
|
|
618
|
-
aria-checked={notificationListenerEnabled}
|
|
619
|
-
>
|
|
620
|
-
<span className="toggle-switch-thumb" />
|
|
621
|
-
</button>
|
|
622
|
-
</label>
|
|
623
|
-
<label className="drawer-toggle">
|
|
624
|
-
<span className="drawer-toggle-label">SMS Access</span>
|
|
625
|
-
<button
|
|
626
|
-
className={`toggle-switch ${smsEnabled ? "toggle-switch-on" : ""}`}
|
|
627
|
-
onClick={handleSmsToggle}
|
|
628
|
-
disabled={togglingSms}
|
|
629
|
-
role="switch"
|
|
630
|
-
aria-checked={smsEnabled}
|
|
631
|
-
>
|
|
632
|
-
<span className="toggle-switch-thumb" />
|
|
633
|
-
</button>
|
|
634
|
-
</label>
|
|
635
|
-
<label className="drawer-toggle">
|
|
636
|
-
<span className="drawer-toggle-label">Contacts Access</span>
|
|
637
|
-
<button
|
|
638
|
-
className={`toggle-switch ${contactsEnabled ? "toggle-switch-on" : ""}`}
|
|
639
|
-
onClick={handleContactsToggle}
|
|
640
|
-
disabled={togglingContacts}
|
|
641
|
-
role="switch"
|
|
642
|
-
aria-checked={contactsEnabled}
|
|
643
|
-
>
|
|
644
|
-
<span className="toggle-switch-thumb" />
|
|
645
|
-
</button>
|
|
646
|
-
</label>
|
|
647
|
-
<label className="drawer-toggle">
|
|
648
|
-
<span className="drawer-toggle-label">Calendar Access</span>
|
|
649
|
-
<button
|
|
650
|
-
className={`toggle-switch ${calendarEnabled ? "toggle-switch-on" : ""}`}
|
|
651
|
-
onClick={handleCalendarToggle}
|
|
652
|
-
disabled={togglingCalendar}
|
|
653
|
-
role="switch"
|
|
654
|
-
aria-checked={calendarEnabled}
|
|
655
|
-
>
|
|
656
|
-
<span className="toggle-switch-thumb" />
|
|
657
|
-
</button>
|
|
658
|
-
</label>
|
|
659
|
-
<label className="drawer-toggle">
|
|
660
|
-
<span className="drawer-toggle-label">Do Not Disturb Control</span>
|
|
661
|
-
<button
|
|
662
|
-
className={`toggle-switch ${dndEnabled ? "toggle-switch-on" : ""}`}
|
|
663
|
-
onClick={handleDndToggle}
|
|
664
|
-
disabled={togglingDnd}
|
|
665
|
-
role="switch"
|
|
666
|
-
aria-checked={dndEnabled}
|
|
667
|
-
>
|
|
668
|
-
<span className="toggle-switch-thumb" />
|
|
669
|
-
</button>
|
|
670
|
-
</label>
|
|
671
|
-
<label className="drawer-toggle">
|
|
672
|
-
<span className="drawer-toggle-label">Alert Access</span>
|
|
673
|
-
<button
|
|
674
|
-
className={`toggle-switch ${alarmEnabled ? "toggle-switch-on" : ""}`}
|
|
675
|
-
onClick={handleAlarmToggle}
|
|
676
|
-
disabled={togglingAlarm}
|
|
677
|
-
role="switch"
|
|
678
|
-
aria-checked={alarmEnabled}
|
|
679
|
-
>
|
|
680
|
-
<span className="toggle-switch-thumb" />
|
|
681
|
-
</button>
|
|
682
|
-
</label>
|
|
683
|
-
<label className="drawer-toggle">
|
|
684
|
-
<span className="drawer-toggle-label">Battery Access</span>
|
|
685
|
-
<button
|
|
686
|
-
className={`toggle-switch ${batteryEnabled ? "toggle-switch-on" : ""}`}
|
|
687
|
-
onClick={handleBatteryToggle}
|
|
688
|
-
disabled={togglingBattery}
|
|
689
|
-
role="switch"
|
|
690
|
-
aria-checked={batteryEnabled}
|
|
691
|
-
>
|
|
692
|
-
<span className="toggle-switch-thumb" />
|
|
693
|
-
</button>
|
|
694
|
-
</label>
|
|
695
|
-
<label className="drawer-toggle">
|
|
696
|
-
<span className="drawer-toggle-label">Email Access</span>
|
|
697
|
-
<button
|
|
698
|
-
className={`toggle-switch ${emailEnabled ? "toggle-switch-on" : ""}`}
|
|
699
|
-
onClick={handleEmailToggle}
|
|
700
|
-
disabled={togglingEmail}
|
|
701
|
-
role="switch"
|
|
702
|
-
aria-checked={emailEnabled}
|
|
703
|
-
>
|
|
704
|
-
<span className="toggle-switch-thumb" />
|
|
705
|
-
</button>
|
|
706
|
-
</label>
|
|
389
|
+
{CAPABILITIES.filter(isCapabilityVisible).map((definition) => {
|
|
390
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
391
|
+
return (
|
|
392
|
+
<label key={definition.capability} className="drawer-toggle">
|
|
393
|
+
<span className="drawer-toggle-label">{definition.label}</span>
|
|
394
|
+
<button
|
|
395
|
+
className={`toggle-switch ${enabled ? "toggle-switch-on" : ""}`}
|
|
396
|
+
onClick={() => toggleCapability(definition)}
|
|
397
|
+
disabled={togglingCapability === definition.capability}
|
|
398
|
+
role="switch"
|
|
399
|
+
aria-checked={enabled}
|
|
400
|
+
>
|
|
401
|
+
<span className="toggle-switch-thumb" />
|
|
402
|
+
</button>
|
|
403
|
+
</label>
|
|
404
|
+
);
|
|
405
|
+
})}
|
|
707
406
|
</div>
|
|
708
407
|
</>
|
|
709
408
|
)}
|