@peers-app/peers-ui 0.8.0 → 0.8.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.
Files changed (30) hide show
  1. package/dist/command-palette/command-palette.d.ts +2 -2
  2. package/dist/command-palette/command-palette.js +3 -7
  3. package/dist/components/group-switcher.d.ts +2 -1
  4. package/dist/components/group-switcher.js +7 -6
  5. package/dist/screens/network-viewer/device-details-modal.js +44 -0
  6. package/dist/screens/network-viewer/group-details-modal.js +80 -2
  7. package/dist/screens/network-viewer/network-viewer.js +36 -16
  8. package/dist/screens/settings/settings-page.js +13 -7
  9. package/dist/screens/setup-user.js +8 -6
  10. package/dist/system-apps/index.d.ts +1 -0
  11. package/dist/system-apps/index.js +10 -1
  12. package/dist/system-apps/mobile-settings.app.d.ts +2 -0
  13. package/dist/system-apps/mobile-settings.app.js +8 -0
  14. package/dist/tabs-layout/tabs-layout.js +60 -38
  15. package/dist/tabs-layout/tabs-state.d.ts +10 -4
  16. package/dist/tabs-layout/tabs-state.js +41 -4
  17. package/dist/ui-router/ui-loader.js +45 -12
  18. package/package.json +3 -3
  19. package/src/command-palette/command-palette.ts +4 -8
  20. package/src/components/group-switcher.tsx +12 -8
  21. package/src/screens/network-viewer/device-details-modal.tsx +55 -0
  22. package/src/screens/network-viewer/group-details-modal.tsx +144 -1
  23. package/src/screens/network-viewer/network-viewer.tsx +36 -29
  24. package/src/screens/settings/settings-page.tsx +17 -9
  25. package/src/screens/setup-user.tsx +9 -6
  26. package/src/system-apps/index.ts +9 -0
  27. package/src/system-apps/mobile-settings.app.ts +8 -0
  28. package/src/tabs-layout/tabs-layout.tsx +108 -82
  29. package/src/tabs-layout/tabs-state.ts +54 -5
  30. package/src/ui-router/ui-loader.tsx +50 -11
@@ -8,8 +8,8 @@ export interface Command {
8
8
  action: () => void;
9
9
  isAvailable?: () => boolean;
10
10
  }
11
- export declare const isCommandPaletteOpen: import("@peers-app/peers-sdk").PersistentVar<boolean>;
12
- export declare const commandSearchQuery: import("@peers-app/peers-sdk").PersistentVar<string>;
11
+ export declare const isCommandPaletteOpen: import("@peers-app/peers-sdk").Observable<boolean>;
12
+ export declare const commandSearchQuery: import("@peers-app/peers-sdk").Observable<string>;
13
13
  export declare function registerCommand(command: Command): void;
14
14
  export declare function unregisterCommand(commandId: string): void;
15
15
  export declare function getCommand(commandId: string): Command | undefined;
@@ -47,13 +47,9 @@ exports.initializeCommandPalette = initializeCommandPalette;
47
47
  exports.destroyCommandPalette = destroyCommandPalette;
48
48
  const peers_sdk_1 = require("@peers-app/peers-sdk");
49
49
  const tabs_state_1 = require("../tabs-layout/tabs-state");
50
- // Command palette state (using device scope but with temporary defaults)
51
- exports.isCommandPaletteOpen = (0, peers_sdk_1.deviceVar)('commandPaletteOpen', {
52
- defaultValue: false,
53
- });
54
- exports.commandSearchQuery = (0, peers_sdk_1.deviceVar)('commandSearchQuery', {
55
- defaultValue: '',
56
- });
50
+ // Command palette state (in-memory only, no persistence needed)
51
+ exports.isCommandPaletteOpen = (0, peers_sdk_1.observable)(false);
52
+ exports.commandSearchQuery = (0, peers_sdk_1.observable)('');
57
53
  // Command registry
58
54
  const registeredCommands = new Map();
59
55
  // Detect platform for keyboard shortcuts
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  interface GroupSwitcherProps {
3
3
  colorMode: string;
4
+ isMobile?: boolean;
4
5
  }
5
- export declare function GroupSwitcher({ colorMode }: GroupSwitcherProps): React.JSX.Element;
6
+ export declare function GroupSwitcher({ colorMode, isMobile }: GroupSwitcherProps): React.JSX.Element;
6
7
  export {};
@@ -37,7 +37,7 @@ exports.GroupSwitcher = GroupSwitcher;
37
37
  const peers_sdk_1 = require("@peers-app/peers-sdk");
38
38
  const react_1 = __importStar(require("react"));
39
39
  const hooks_1 = require("../hooks");
40
- function GroupSwitcher({ colorMode }) {
40
+ function GroupSwitcher({ colorMode, isMobile = false }) {
41
41
  const [showDropdown, setShowDropdown] = (0, react_1.useState)(false);
42
42
  const [showCreateModal, setShowCreateModal] = (0, react_1.useState)(false);
43
43
  const [allGroups, setAllGroups] = (0, react_1.useState)([]);
@@ -84,13 +84,14 @@ function GroupSwitcher({ colorMode }) {
84
84
  const isDark = colorMode === 'dark';
85
85
  return (react_1.default.createElement(react_1.default.Fragment, null,
86
86
  react_1.default.createElement("div", { className: "dropdown" },
87
- react_1.default.createElement("button", { className: "btn btn-sm me-2 d-flex align-items-center", onClick: () => setShowDropdown(!showDropdown), title: `Current group: ${getGroupName(currentGroup)}`, style: {
88
- padding: '4px 8px',
87
+ react_1.default.createElement("button", { className: `btn btn-sm ${isMobile ? '' : 'me-2'} d-flex align-items-center`, onClick: () => setShowDropdown(!showDropdown), title: `Current group: ${getGroupName(currentGroup)}`, style: {
88
+ padding: isMobile ? '4px' : '4px 8px',
89
89
  fontSize: '12px',
90
90
  borderRadius: '6px',
91
91
  border: 'none',
92
92
  background: 'transparent',
93
- color: isDark ? '#adb5bd' : '#6c757d'
93
+ color: isDark ? '#adb5bd' : '#6c757d',
94
+ minWidth: isMobile ? '36px' : undefined
94
95
  }, onMouseEnter: (e) => {
95
96
  e.currentTarget.style.backgroundColor = isDark ? '#495057' : '#f8f9fa';
96
97
  e.currentTarget.style.color = isDark ? '#ffffff' : '#0d6efd';
@@ -98,8 +99,8 @@ function GroupSwitcher({ colorMode }) {
98
99
  e.currentTarget.style.backgroundColor = 'transparent';
99
100
  e.currentTarget.style.color = isDark ? '#adb5bd' : '#6c757d';
100
101
  } },
101
- react_1.default.createElement("i", { className: `${getGroupIcon(currentGroup)} me-1`, style: { fontSize: '14px' } }),
102
- react_1.default.createElement("span", { className: "text-truncate", style: { maxWidth: '80px' } }, getGroupName(currentGroup)),
102
+ react_1.default.createElement("i", { className: `${getGroupIcon(currentGroup)} ${isMobile ? '' : 'me-1'}`, style: { fontSize: '14px' } }),
103
+ !isMobile && (react_1.default.createElement("span", { className: "text-truncate", style: { maxWidth: '80px' } }, getGroupName(currentGroup))),
103
104
  react_1.default.createElement("i", { className: "bi-chevron-down ms-1", style: { fontSize: '10px' } })),
104
105
  showDropdown && (react_1.default.createElement("div", { className: `dropdown-menu show position-absolute ${isDark ? 'dropdown-menu-dark' : ''}`, style: {
105
106
  left: 0,
@@ -38,6 +38,27 @@ const react_1 = __importStar(require("react"));
38
38
  const peers_sdk_1 = require("@peers-app/peers-sdk");
39
39
  const loading_indicator_1 = require("../../components/loading-indicator");
40
40
  const trust_level_badge_1 = require("../../components/trust-level-badge");
41
+ /** Format bytes to human-readable string (KB, MB, GB) */
42
+ function formatBytes(bytes) {
43
+ if (bytes < 1024)
44
+ return `${bytes} B`;
45
+ if (bytes < 1024 * 1024)
46
+ return `${(bytes / 1024).toFixed(1)} KB`;
47
+ if (bytes < 1024 * 1024 * 1024)
48
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
49
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
50
+ }
51
+ /** Format transfer rate to human-readable string (KB/s or MB/s) */
52
+ function formatRate(mbps) {
53
+ if (mbps === undefined || mbps === 0)
54
+ return '0 KB/s';
55
+ // Show KB/s if less than 0.1 MB/s (100 KB/s)
56
+ if (mbps < 0.1) {
57
+ const kbps = mbps * 1024;
58
+ return `${kbps.toFixed(1)} KB/s`;
59
+ }
60
+ return `${mbps.toFixed(2)} MB/s`;
61
+ }
41
62
  function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
42
63
  const [device, setDevice] = (0, react_1.useState)(null);
43
64
  const [loading, setLoading] = (0, react_1.useState)(true);
@@ -133,6 +154,29 @@ function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
133
154
  react_1.default.createElement("strong", null, "Path:"),
134
155
  react_1.default.createElement("br", null),
135
156
  react_1.default.createElement("small", { className: "text-muted" }, device.throughDeviceIds.join(' → ')))))),
157
+ react_1.default.createElement("div", { className: "mb-4" },
158
+ react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" }, "Throughput"),
159
+ react_1.default.createElement("div", { className: "row" },
160
+ react_1.default.createElement("div", { className: "col-md-6 mb-3" },
161
+ react_1.default.createElement("strong", null, "Send Rate:"),
162
+ react_1.default.createElement("br", null),
163
+ react_1.default.createElement("span", { className: "text-success" },
164
+ "\u2191 ",
165
+ formatRate(device.sendRateMBps))),
166
+ react_1.default.createElement("div", { className: "col-md-6 mb-3" },
167
+ react_1.default.createElement("strong", null, "Receive Rate:"),
168
+ react_1.default.createElement("br", null),
169
+ react_1.default.createElement("span", { className: "text-primary" },
170
+ "\u2193 ",
171
+ formatRate(device.receiveRateMBps))),
172
+ react_1.default.createElement("div", { className: "col-md-6 mb-3" },
173
+ react_1.default.createElement("strong", null, "Total Sent:"),
174
+ react_1.default.createElement("br", null),
175
+ react_1.default.createElement("span", null, formatBytes(device.bytesSent || 0))),
176
+ react_1.default.createElement("div", { className: "col-md-6 mb-3" },
177
+ react_1.default.createElement("strong", null, "Total Received:"),
178
+ react_1.default.createElement("br", null),
179
+ react_1.default.createElement("span", null, formatBytes(device.bytesReceived || 0))))),
136
180
  device.sharedGroups.length > 0 && (react_1.default.createElement("div", { className: "mb-4" },
137
181
  react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" },
138
182
  "Shared Groups (",
@@ -40,8 +40,12 @@ const loading_indicator_1 = require("../../components/loading-indicator");
40
40
  const trust_level_badge_1 = require("../../components/trust-level-badge");
41
41
  function GroupDetailsModal({ groupId, onClose }) {
42
42
  const [group, setGroup] = (0, react_1.useState)(null);
43
+ const [syncInfo, setSyncInfo] = (0, react_1.useState)(null);
43
44
  const [loading, setLoading] = (0, react_1.useState)(true);
44
45
  const [notFound, setNotFound] = (0, react_1.useState)(false);
46
+ const [downloading, setDownloading] = (0, react_1.useState)(false);
47
+ const [downloadProgress, setDownloadProgress] = (0, react_1.useState)(null);
48
+ const [downloadResult, setDownloadResult] = (0, react_1.useState)(null);
45
49
  const loadData = async () => {
46
50
  try {
47
51
  const api = window.electronAPI?.networkViewer;
@@ -49,7 +53,10 @@ function GroupDetailsModal({ groupId, onClose }) {
49
53
  console.warn('Network Viewer API not available');
50
54
  return;
51
55
  }
52
- const groupData = await api.getGroupDetails(groupId);
56
+ const [groupData, syncData] = await Promise.all([
57
+ api.getGroupDetails(groupId),
58
+ api.getGroupSyncStatus(groupId)
59
+ ]);
53
60
  if (!groupData) {
54
61
  setNotFound(true);
55
62
  }
@@ -57,6 +64,9 @@ function GroupDetailsModal({ groupId, onClose }) {
57
64
  setGroup(groupData);
58
65
  setNotFound(false);
59
66
  }
67
+ if (syncData && syncData.length > 0) {
68
+ setSyncInfo(syncData[0]);
69
+ }
60
70
  }
61
71
  catch (error) {
62
72
  console.error('Error loading group details:', error);
@@ -66,6 +76,38 @@ function GroupDetailsModal({ groupId, onClose }) {
66
76
  setLoading(false);
67
77
  }
68
78
  };
79
+ const handleDownloadDatabase = async (deviceId) => {
80
+ try {
81
+ setDownloading(true);
82
+ setDownloadProgress('Preparing download...');
83
+ setDownloadResult(null);
84
+ const api = window.electronAPI?.networkViewer;
85
+ if (!api?.downloadRemoteDatabase) {
86
+ setDownloadResult({
87
+ success: false,
88
+ message: 'Download API not available'
89
+ });
90
+ return;
91
+ }
92
+ setDownloadProgress('Downloading database...');
93
+ const result = await api.downloadRemoteDatabase(groupId, deviceId);
94
+ setDownloadResult(result);
95
+ if (result.success) {
96
+ setDownloadProgress(null);
97
+ }
98
+ }
99
+ catch (error) {
100
+ console.error('Error downloading database:', error);
101
+ setDownloadResult({
102
+ success: false,
103
+ message: error?.message || 'Download failed'
104
+ });
105
+ }
106
+ finally {
107
+ setDownloading(false);
108
+ setDownloadProgress(null);
109
+ }
110
+ };
69
111
  (0, react_1.useEffect)(() => {
70
112
  loadData();
71
113
  }, [groupId]);
@@ -123,5 +165,41 @@ function GroupDetailsModal({ groupId, onClose }) {
123
165
  react_1.default.createElement("td", null,
124
166
  react_1.default.createElement("span", { className: "badge bg-primary" }, member.role)),
125
167
  react_1.default.createElement("td", null,
126
- react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown }))))))))))))))));
168
+ react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown })))))))))),
169
+ react_1.default.createElement("div", { className: "mb-4" },
170
+ react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" },
171
+ react_1.default.createElement("i", { className: "bi bi-download me-2" }),
172
+ "Download Database (Fast Sync)"),
173
+ react_1.default.createElement("p", { className: "text-muted small mb-3" }, "Download the entire database from a connected device. This is useful for fast initial sync on a new device. The database will be saved as a backup file."),
174
+ syncInfo && syncInfo.connectedDevices.length > 0 ? (react_1.default.createElement("div", null,
175
+ react_1.default.createElement("label", { className: "form-label" }, "Select a connected device:"),
176
+ react_1.default.createElement("div", { className: "d-flex flex-wrap gap-2" }, syncInfo.connectedDevices.map((device) => (react_1.default.createElement("button", { key: device.deviceId, className: "btn btn-outline-primary btn-sm", onClick: () => handleDownloadDatabase(device.deviceId), disabled: downloading }, downloading ? (react_1.default.createElement(react_1.default.Fragment, null,
177
+ react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-1", role: "status", "aria-hidden": "true" }),
178
+ "Downloading...")) : (react_1.default.createElement(react_1.default.Fragment, null,
179
+ react_1.default.createElement("i", { className: "bi bi-cloud-download me-1" }),
180
+ device.deviceId.substring(0, 8),
181
+ "...",
182
+ react_1.default.createElement("span", { className: "badge bg-secondary ms-1" },
183
+ Math.round(device.latencyMs),
184
+ "ms"))))))))) : (react_1.default.createElement("div", { className: "alert alert-info mb-0" },
185
+ react_1.default.createElement("i", { className: "bi bi-info-circle me-2" }),
186
+ "No devices are currently connected to this group. Connect to a device first to download its database.")),
187
+ downloadProgress && (react_1.default.createElement("div", { className: "mt-3" },
188
+ react_1.default.createElement("div", { className: "d-flex align-items-center" },
189
+ react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2", role: "status", "aria-hidden": "true" }),
190
+ react_1.default.createElement("span", null, downloadProgress)))),
191
+ downloadResult && (react_1.default.createElement("div", { className: `alert mt-3 mb-0 ${downloadResult.success ? 'alert-success' : 'alert-danger'}` }, downloadResult.success ? (react_1.default.createElement(react_1.default.Fragment, null,
192
+ react_1.default.createElement("i", { className: "bi bi-check-circle me-2" }),
193
+ react_1.default.createElement("strong", null, "Download complete!"),
194
+ react_1.default.createElement("br", null),
195
+ react_1.default.createElement("small", null,
196
+ "File: ",
197
+ react_1.default.createElement("code", null, downloadResult.filePath),
198
+ react_1.default.createElement("br", null),
199
+ "Size: ",
200
+ downloadResult.size ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB` : 'Unknown'))) : (react_1.default.createElement(react_1.default.Fragment, null,
201
+ react_1.default.createElement("i", { className: "bi bi-exclamation-circle me-2" }),
202
+ react_1.default.createElement("strong", null, "Download failed:"),
203
+ " ",
204
+ downloadResult.message)))))))))));
127
205
  }
@@ -43,6 +43,27 @@ const globals_1 = require("../../globals");
43
43
  const device_details_modal_1 = require("./device-details-modal");
44
44
  const group_details_modal_1 = require("./group-details-modal");
45
45
  const usage_graph_1 = require("./usage-graph");
46
+ /** Format bytes to human-readable string (KB, MB, GB) */
47
+ function formatBytes(bytes) {
48
+ if (bytes < 1024)
49
+ return `${bytes} B`;
50
+ if (bytes < 1024 * 1024)
51
+ return `${(bytes / 1024).toFixed(1)} KB`;
52
+ if (bytes < 1024 * 1024 * 1024)
53
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
54
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
55
+ }
56
+ /** Format transfer rate to human-readable string (KB/s or MB/s) */
57
+ function formatRate(mbps) {
58
+ if (mbps === undefined || mbps === 0)
59
+ return '0 KB/s';
60
+ // Show KB/s if less than 0.1 MB/s (100 KB/s)
61
+ if (mbps < 0.1) {
62
+ const kbps = mbps * 1024;
63
+ return `${kbps.toFixed(1)} KB/s`;
64
+ }
65
+ return `${mbps.toFixed(2)} MB/s`;
66
+ }
46
67
  function NetworkViewerList() {
47
68
  const [overview, setOverview] = (0, react_1.useState)(null);
48
69
  const [connections, setConnections] = (0, react_1.useState)([]);
@@ -238,22 +259,6 @@ function NetworkViewerList() {
238
259
  { maxValue: 80, color: 'rgba(255, 193, 7, 1)' },
239
260
  { maxValue: Infinity, color: 'rgba(220, 53, 69, 1)' }
240
261
  ] }))))),
241
- overview.groups.length > 0 && (react_1.default.createElement("div", { className: "card mb-4" },
242
- react_1.default.createElement("div", { className: "card-body" },
243
- react_1.default.createElement("h5", { className: "card-title" },
244
- "Groups: ",
245
- overview.groups.length),
246
- react_1.default.createElement("div", { className: "table-responsive" },
247
- react_1.default.createElement("table", { className: "table table-sm" },
248
- react_1.default.createElement("thead", null,
249
- react_1.default.createElement("tr", null,
250
- react_1.default.createElement("th", null, "Group Name"),
251
- react_1.default.createElement("th", null, "Connected Devices"),
252
- react_1.default.createElement("th", null, "Total Members"))),
253
- react_1.default.createElement("tbody", null, overview.groups.map(group => (react_1.default.createElement("tr", { key: group.groupId },
254
- react_1.default.createElement("td", null, group.groupName),
255
- react_1.default.createElement("td", null, group.connectedDevices),
256
- react_1.default.createElement("td", null, group.totalMembers)))))))))),
257
262
  react_1.default.createElement("div", { className: "card mb-4" },
258
263
  react_1.default.createElement("div", { className: "card-body" },
259
264
  react_1.default.createElement("h5", { className: "card-title" }, "Active Connections"),
@@ -270,6 +275,7 @@ function NetworkViewerList() {
270
275
  react_1.default.createElement("th", null, "Shared Groups"),
271
276
  react_1.default.createElement("th", null, "Latency"),
272
277
  react_1.default.createElement("th", null, "Error Rate"),
278
+ react_1.default.createElement("th", null, "Throughput"),
273
279
  react_1.default.createElement("th", null, "Actions"))),
274
280
  react_1.default.createElement("tbody", null, connections.map(conn => {
275
281
  return (react_1.default.createElement("tr", { key: conn.deviceId },
@@ -300,6 +306,20 @@ function NetworkViewerList() {
300
306
  conn.errorRate < 0.2 ? 'warning' : 'danger'}` },
301
307
  (conn.errorRate * 100).toFixed(1),
302
308
  "%")),
309
+ react_1.default.createElement("td", null,
310
+ react_1.default.createElement("small", null,
311
+ react_1.default.createElement("span", { className: "text-success" },
312
+ "\u2191",
313
+ formatRate(conn.sendRateMBps)),
314
+ ' / ',
315
+ react_1.default.createElement("span", { className: "text-primary" },
316
+ "\u2193",
317
+ formatRate(conn.receiveRateMBps))),
318
+ react_1.default.createElement("br", null),
319
+ react_1.default.createElement("small", { className: "text-muted" },
320
+ formatBytes(conn.bytesSent || 0),
321
+ " / ",
322
+ formatBytes(conn.bytesReceived || 0))),
303
323
  react_1.default.createElement("td", null,
304
324
  react_1.default.createElement("button", { className: "btn btn-sm btn-outline-primary me-2", onClick: () => handleViewDetails(conn.deviceId), title: "View Details" },
305
325
  react_1.default.createElement("i", { className: "bi bi-info-circle" })),
@@ -34,12 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SettingsPage = void 0;
37
- const react_1 = __importStar(require("react"));
38
37
  const peers_sdk_1 = require("@peers-app/peers-sdk");
39
- const tooltip_1 = require("../../components/tooltip");
40
- const color_mode_dropdown_1 = require("./color-mode-dropdown");
38
+ const react_1 = __importStar(require("react"));
41
39
  const input_1 = require("../../components/input");
40
+ const tooltip_1 = require("../../components/tooltip");
42
41
  const hooks_1 = require("../../hooks");
42
+ const color_mode_dropdown_1 = require("./color-mode-dropdown");
43
43
  const SettingsPage = () => {
44
44
  return (react_1.default.createElement("div", { className: 'container-fluid' },
45
45
  react_1.default.createElement(color_mode_dropdown_1.ColorModeDropdown, null),
@@ -71,13 +71,19 @@ const DeviceName = () => {
71
71
  }, [deviceId]);
72
72
  const handleSave = async () => {
73
73
  try {
74
- if (!deviceId)
74
+ if (!deviceId) {
75
+ console.error('No device id');
75
76
  return;
77
+ }
76
78
  const userContext = await (0, peers_sdk_1.getUserContext)();
77
79
  const devicesTable = (0, peers_sdk_1.Devices)(userContext.userDataContext);
78
- const device = await devicesTable.get(deviceId);
79
- if (!device)
80
- return;
80
+ const device = await devicesTable.get(deviceId) || {
81
+ deviceId,
82
+ userId: userContext.userId,
83
+ firstSeen: new Date(),
84
+ lastSeen: new Date(),
85
+ trustLevel: peers_sdk_1.TrustLevel.NewDevice,
86
+ };
81
87
  device.name = deviceName() || undefined;
82
88
  await devicesTable.save(device);
83
89
  currentDeviceName(deviceName());
@@ -103,7 +103,7 @@ const SetupUser = () => {
103
103
  };
104
104
  // Step 1: Select User Type
105
105
  if (step === 'select-user-type' && isExistingUser === null) {
106
- return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
106
+ return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
107
107
  react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
108
108
  react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
109
109
  react_1.default.createElement("div", { className: "text-center mb-4" },
@@ -125,7 +125,7 @@ const SetupUser = () => {
125
125
  }
126
126
  // Step 2: New User Confirmation
127
127
  if (isExistingUser === false) {
128
- return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
128
+ return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
129
129
  react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
130
130
  react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
131
131
  react_1.default.createElement("div", { className: "text-center mb-4" },
@@ -147,12 +147,14 @@ const SetupUser = () => {
147
147
  }
148
148
  // Step 3: Existing User Sign In
149
149
  if (isExistingUser === true) {
150
- return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
150
+ return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
151
151
  react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
152
152
  react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
153
- react_1.default.createElement("div", { className: "text-center mb-4" },
154
- react_1.default.createElement("i", { className: "bi bi-box-arrow-in-right text-primary fs-1 mb-3 d-block" }),
155
- react_1.default.createElement("h3", { className: `fw-bold mb-2 ${isDark ? 'text-light' : ''}` }, "Sign In"),
153
+ react_1.default.createElement("div", { className: "text-center mb-2" },
154
+ react_1.default.createElement("h3", { className: `fw-bold mb-2 ${isDark ? 'text-light' : ''}` },
155
+ react_1.default.createElement("span", null, "Sign In"),
156
+ "\u00A0\u00A0",
157
+ react_1.default.createElement("i", { className: "bi bi-box-arrow-in-right text-primary" })),
156
158
  react_1.default.createElement("p", { className: isDark ? 'text-light opacity-75' : 'text-muted' }, "Enter your existing credentials")),
157
159
  error && (react_1.default.createElement("div", { className: "alert alert-danger", role: "alert" },
158
160
  react_1.default.createElement("i", { className: "bi bi-exclamation-triangle me-2" }),
@@ -18,5 +18,6 @@ export { contactsApp } from './contacts.app';
18
18
  export { consoleLogsApp } from './console-logs.app';
19
19
  export { networkViewerApp } from './network-viewer.app';
20
20
  export { dataExplorerApp } from './data-explorer.app';
21
+ export { mobileSettingsApp } from './mobile-settings.app';
21
22
  export declare const systemApps: IAppNav[];
22
23
  export declare const systemPackage: IPackage;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.systemPackage = exports.systemApps = exports.dataExplorerApp = exports.networkViewerApp = exports.consoleLogsApp = exports.contactsApp = exports.groupsApp = exports.settingsApp = exports.profileApp = exports.predicatesApp = exports.knowledgeFramesApp = exports.knowledgeValuesApp = exports.threadsApp = exports.packagesApp = exports.typesApp = exports.variablesApp = exports.eventsApp = exports.workflowsApp = exports.toolsApp = exports.assistantsApp = exports.searchApp = void 0;
3
+ exports.systemPackage = exports.systemApps = exports.mobileSettingsApp = exports.dataExplorerApp = exports.networkViewerApp = exports.consoleLogsApp = exports.contactsApp = exports.groupsApp = exports.settingsApp = exports.profileApp = exports.predicatesApp = exports.knowledgeFramesApp = exports.knowledgeValuesApp = exports.threadsApp = exports.packagesApp = exports.typesApp = exports.variablesApp = exports.eventsApp = exports.workflowsApp = exports.toolsApp = exports.assistantsApp = exports.searchApp = void 0;
4
4
  // Import all system apps
5
5
  var search_app_1 = require("./search.app");
6
6
  Object.defineProperty(exports, "searchApp", { enumerable: true, get: function () { return search_app_1.searchApp; } });
@@ -40,6 +40,8 @@ var network_viewer_app_1 = require("./network-viewer.app");
40
40
  Object.defineProperty(exports, "networkViewerApp", { enumerable: true, get: function () { return network_viewer_app_1.networkViewerApp; } });
41
41
  var data_explorer_app_1 = require("./data-explorer.app");
42
42
  Object.defineProperty(exports, "dataExplorerApp", { enumerable: true, get: function () { return data_explorer_app_1.dataExplorerApp; } });
43
+ var mobile_settings_app_1 = require("./mobile-settings.app");
44
+ Object.defineProperty(exports, "mobileSettingsApp", { enumerable: true, get: function () { return mobile_settings_app_1.mobileSettingsApp; } });
43
45
  // Import individual apps
44
46
  const search_app_2 = require("./search.app");
45
47
  const assistants_app_2 = require("./assistants.app");
@@ -60,6 +62,11 @@ const contacts_app_2 = require("./contacts.app");
60
62
  const console_logs_app_2 = require("./console-logs.app");
61
63
  const network_viewer_app_2 = require("./network-viewer.app");
62
64
  const data_explorer_app_2 = require("./data-explorer.app");
65
+ const mobile_settings_app_2 = require("./mobile-settings.app");
66
+ // Helper to check if running in React Native
67
+ function isReactNative() {
68
+ return typeof window.__NATIVE_THEME !== 'undefined';
69
+ }
63
70
  // Collection of all system apps
64
71
  exports.systemApps = [
65
72
  // Core Navigation & Search
@@ -82,6 +89,8 @@ exports.systemApps = [
82
89
  // User & Settings Apps
83
90
  profile_app_2.profileApp,
84
91
  settings_app_2.settingsApp,
92
+ // Mobile Settings (only in React Native)
93
+ ...(isReactNative() ? [mobile_settings_app_2.mobileSettingsApp] : []),
85
94
  // System Tools & Debugging
86
95
  console_logs_app_2.consoleLogsApp,
87
96
  network_viewer_app_2.networkViewerApp,
@@ -0,0 +1,2 @@
1
+ import { IAppNav } from "@peers-app/peers-sdk";
2
+ export declare const mobileSettingsApp: IAppNav;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mobileSettingsApp = void 0;
4
+ exports.mobileSettingsApp = {
5
+ name: 'Mobile Settings',
6
+ iconClassName: 'bi-phone-fill',
7
+ navigationPath: 'mobile-settings'
8
+ };
@@ -134,7 +134,7 @@ function TabsLayoutInternal() {
134
134
  borderBottomWidth: '1px',
135
135
  borderBottomColor: _colorMode === 'light' ? '#dee2e6' : '#495057'
136
136
  } }, isMobile ? (react_1.default.createElement(MobileTabsHeader, { tabs: tabs, activeTab: activeTab, onSwitch: switchTab, onClose: closeTab, colorMode: _colorMode })) : (react_1.default.createElement("div", { className: "d-flex align-items-center px-1", style: { height: '36px' } },
137
- react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: _colorMode }),
137
+ react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: _colorMode, isMobile: false }),
138
138
  react_1.default.createElement("button", { className: "btn btn-sm me-2 d-flex align-items-center", onClick: command_palette_1.openCommandPalette, title: "Search everything (Cmd+K)", style: {
139
139
  padding: '4px 8px',
140
140
  fontSize: '12px',
@@ -158,47 +158,52 @@ function TabsLayoutInternal() {
158
158
  react_1.default.createElement(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId })))))));
159
159
  }
160
160
  function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode }) {
161
- const [showTabDropdown, setShowTabDropdown] = react_1.default.useState(false);
161
+ const [showMenuDropdown, setShowMenuDropdown] = react_1.default.useState(false);
162
162
  const currentTab = tabs.find(t => t.tabId === activeTab);
163
163
  const nonLauncherTabs = tabs.filter(t => t.packageId !== 'launcher');
164
- return (react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between px-2", style: { height: '36px' } },
164
+ return (react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between px-2 position-relative", style: { height: '36px' } },
165
165
  react_1.default.createElement("div", { className: "d-flex align-items-center gap-1" },
166
- react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: colorMode }),
167
- react_1.default.createElement("button", { className: "btn btn-sm", onClick: command_palette_1.openCommandPalette, title: "Search everything", style: {
168
- minWidth: '36px',
169
- border: 'none',
170
- background: 'transparent',
171
- color: colorMode === 'light' ? '#6c757d' : '#adb5bd'
172
- }, onMouseEnter: (e) => {
173
- e.currentTarget.style.backgroundColor = colorMode === 'light' ? '#f8f9fa' : '#495057';
174
- e.currentTarget.style.color = colorMode === 'light' ? '#0d6efd' : '#ffffff';
175
- }, onMouseLeave: (e) => {
176
- e.currentTarget.style.backgroundColor = 'transparent';
177
- e.currentTarget.style.color = colorMode === 'light' ? '#6c757d' : '#adb5bd';
178
- } },
179
- react_1.default.createElement("i", { className: "bi-search" })),
180
- react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-primary' : 'btn-outline-light'}`, onClick: () => onSwitch('launcher'), style: { minWidth: '36px' } },
181
- react_1.default.createElement("i", { className: "bi-grid-3x3-gap" }))),
182
- react_1.default.createElement("div", { className: "d-flex align-items-center flex-grow-1 justify-content-center" },
166
+ react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: colorMode, isMobile: true })),
167
+ react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-center position-absolute", style: {
168
+ left: '50%',
169
+ transform: 'translateX(-50%)',
170
+ pointerEvents: 'none',
171
+ maxWidth: 'calc(100% - 140px)'
172
+ } },
183
173
  currentTab?.iconClassName && currentTab.packageId !== 'launcher' && (react_1.default.createElement("i", { className: `${currentTab.iconClassName} me-2` })),
184
- react_1.default.createElement("span", { className: "fw-medium text-truncate me-2 small" }, currentTab?.title || 'Apps')),
185
- react_1.default.createElement("div", { className: "d-flex align-items-center gap-2" }, nonLauncherTabs.length > 0 && (react_1.default.createElement("div", { className: "dropdown" },
186
- react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-dark' : 'btn-outline-light'}`, onClick: () => setShowTabDropdown(!showTabDropdown) },
187
- react_1.default.createElement("i", { className: "bi-list" }),
188
- react_1.default.createElement("span", { className: "ms-1" }, nonLauncherTabs.length)),
189
- showTabDropdown && (react_1.default.createElement("div", { className: `dropdown-menu show position-absolute ${colorMode === 'light' ? '' : 'dropdown-menu-dark'}`, style: { right: 0, top: '100%', zIndex: 1000, minWidth: '250px' } }, nonLauncherTabs.slice().reverse().map(tab => (react_1.default.createElement("div", { key: tab.tabId, className: `dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? 'active' : ''}`, style: { cursor: 'pointer' }, onClick: () => {
190
- onSwitch(tab.tabId);
191
- setShowTabDropdown(false);
192
- } },
193
- react_1.default.createElement("div", { className: "d-flex align-items-center" },
194
- tab.iconClassName && react_1.default.createElement("i", { className: `${tab.iconClassName} me-2` }),
195
- react_1.default.createElement("span", null, tab.title)),
196
- tab.tabId !== "launcher" && (react_1.default.createElement("button", { className: "btn btn-sm p-0 ms-2", style: { width: '20px', height: '20px' }, onClick: (e) => {
197
- e.stopPropagation();
198
- onClose(tab.tabId);
199
- } },
200
- react_1.default.createElement("i", { className: "bi-x" }))))))))))),
201
- showTabDropdown && (react_1.default.createElement("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowTabDropdown(false) }))));
174
+ react_1.default.createElement("span", { className: "fw-medium text-truncate small" }, currentTab?.title || 'Apps')),
175
+ react_1.default.createElement("div", { className: "d-flex align-items-center" },
176
+ react_1.default.createElement("div", { className: "dropdown" },
177
+ react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-dark' : 'btn-outline-light'}`, onClick: () => setShowMenuDropdown(!showMenuDropdown), style: { minWidth: '36px' } },
178
+ react_1.default.createElement("i", { className: "bi-list" }),
179
+ nonLauncherTabs.length > 0 && (react_1.default.createElement("span", { className: "ms-1" }, nonLauncherTabs.length))),
180
+ showMenuDropdown && (react_1.default.createElement("div", { className: `dropdown-menu show position-absolute ${colorMode === 'light' ? '' : 'dropdown-menu-dark'}`, style: { right: 0, top: '100%', zIndex: 1000, minWidth: '250px' } },
181
+ react_1.default.createElement("div", { className: `dropdown-item d-flex align-items-center ${activeTab === 'launcher' ? 'active' : ''}`, style: { cursor: 'pointer' }, onClick: () => {
182
+ onSwitch('launcher');
183
+ setShowMenuDropdown(false);
184
+ } },
185
+ react_1.default.createElement("i", { className: "bi-grid-3x3-gap me-2" }),
186
+ react_1.default.createElement("span", null, "Apps")),
187
+ react_1.default.createElement("div", { className: "dropdown-item d-flex align-items-center", style: { cursor: 'pointer' }, onClick: () => {
188
+ (0, command_palette_1.openCommandPalette)();
189
+ setShowMenuDropdown(false);
190
+ } },
191
+ react_1.default.createElement("i", { className: "bi-search me-2" }),
192
+ react_1.default.createElement("span", null, "Search")),
193
+ nonLauncherTabs.length > 0 && (react_1.default.createElement("div", { className: "dropdown-divider" })),
194
+ nonLauncherTabs.slice().reverse().map(tab => (react_1.default.createElement("div", { key: tab.tabId, className: `dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? 'active' : ''}`, style: { cursor: 'pointer' }, onClick: () => {
195
+ onSwitch(tab.tabId);
196
+ setShowMenuDropdown(false);
197
+ } },
198
+ react_1.default.createElement("div", { className: "d-flex align-items-center" },
199
+ tab.iconClassName && react_1.default.createElement("i", { className: `${tab.iconClassName} me-2` }),
200
+ react_1.default.createElement("span", null, tab.title)),
201
+ react_1.default.createElement("button", { className: "btn btn-sm p-0 ms-2", style: { width: '20px', height: '20px' }, onClick: (e) => {
202
+ e.stopPropagation();
203
+ onClose(tab.tabId);
204
+ } },
205
+ react_1.default.createElement("i", { className: "bi-x" }))))))))),
206
+ showMenuDropdown && (react_1.default.createElement("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowMenuDropdown(false) }))));
202
207
  }
203
208
  function TabHeader({ tab, isActive, onSwitch, onClose, colorMode }) {
204
209
  const activeClass = isActive
@@ -318,6 +323,23 @@ function AppLauncherTab({ isMobile }) {
318
323
  .map(path => filteredApps.find(app => app.path === path))
319
324
  .filter(Boolean);
320
325
  const openApp = (appItem) => {
326
+ // Check if this is the mobile-settings app and we're in React Native
327
+ if (appItem.path === 'mobile-settings' && typeof window.__NATIVE_THEME !== 'undefined') {
328
+ // Use expo-linking to navigate to native screen
329
+ // @ts-ignore
330
+ if (window.ReactNativeWebView) {
331
+ // @ts-ignore
332
+ window.ReactNativeWebView.postMessage(JSON.stringify({
333
+ type: 'navigate',
334
+ path: 'mobile-settings'
335
+ }));
336
+ }
337
+ else {
338
+ // Fallback to deep link
339
+ window.location.href = 'peers://mobile-settings';
340
+ }
341
+ return;
342
+ }
321
343
  (0, tabs_state_1.goToTabPath)(appItem.path);
322
344
  };
323
345
  return (react_1.default.createElement("div", { className: `container-fluid ${isMobile ? 'p-2' : 'p-4'}`, style: { maxHeight: '100%', overflowY: 'auto' } },