@peers-app/peers-ui 0.7.40 → 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/command-palette/command-palette.d.ts +2 -2
- package/dist/command-palette/command-palette.js +3 -7
- package/dist/components/group-switcher.d.ts +2 -1
- package/dist/components/group-switcher.js +7 -6
- package/dist/globals.d.ts +1 -1
- package/dist/screens/contacts/contact-list.js +4 -1
- package/dist/screens/contacts/index.d.ts +2 -0
- package/dist/screens/contacts/index.js +2 -0
- package/dist/screens/contacts/user-connect.d.ts +2 -0
- package/dist/screens/contacts/user-connect.js +312 -0
- package/dist/screens/network-viewer/device-details-modal.js +44 -0
- package/dist/screens/network-viewer/group-details-modal.js +80 -2
- package/dist/screens/network-viewer/network-viewer.js +36 -16
- package/dist/screens/settings/settings-page.js +13 -7
- package/dist/screens/setup-user.js +8 -6
- package/dist/system-apps/index.d.ts +1 -0
- package/dist/system-apps/index.js +10 -1
- package/dist/system-apps/mobile-settings.app.d.ts +2 -0
- package/dist/system-apps/mobile-settings.app.js +8 -0
- package/dist/tabs-layout/tabs-layout.js +60 -38
- package/dist/tabs-layout/tabs-state.d.ts +10 -4
- package/dist/tabs-layout/tabs-state.js +41 -4
- package/dist/ui-router/ui-loader.js +45 -12
- package/package.json +3 -3
- package/src/command-palette/command-palette.ts +4 -8
- package/src/components/group-switcher.tsx +12 -8
- package/src/screens/contacts/contact-list.tsx +4 -0
- package/src/screens/contacts/index.ts +3 -1
- package/src/screens/contacts/user-connect.tsx +452 -0
- package/src/screens/network-viewer/device-details-modal.tsx +55 -0
- package/src/screens/network-viewer/group-details-modal.tsx +144 -1
- package/src/screens/network-viewer/network-viewer.tsx +36 -29
- package/src/screens/settings/settings-page.tsx +17 -9
- package/src/screens/setup-user.tsx +9 -6
- package/src/system-apps/index.ts +9 -0
- package/src/system-apps/mobile-settings.app.ts +8 -0
- package/src/tabs-layout/tabs-layout.tsx +108 -82
- package/src/tabs-layout/tabs-state.ts +54 -5
- package/src/ui-router/ui-loader.tsx +50 -11
|
@@ -17,6 +17,22 @@ interface IGroupDetails {
|
|
|
17
17
|
}[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
interface IConnectedDevice {
|
|
21
|
+
deviceId: string;
|
|
22
|
+
latencyMs: number;
|
|
23
|
+
errorRate: number;
|
|
24
|
+
timestampLastApplied: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface IGroupSyncInfo {
|
|
28
|
+
groupId: string;
|
|
29
|
+
groupName: string;
|
|
30
|
+
connectedDevices: IConnectedDevice[];
|
|
31
|
+
preferredDeviceIds: string[];
|
|
32
|
+
lastSyncTimestamp: number;
|
|
33
|
+
totalMembers: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
interface IGroupDetailsModalProps {
|
|
21
37
|
groupId: string;
|
|
22
38
|
onClose: () => void;
|
|
@@ -24,8 +40,17 @@ interface IGroupDetailsModalProps {
|
|
|
24
40
|
|
|
25
41
|
export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps) {
|
|
26
42
|
const [group, setGroup] = useState<IGroupDetails | null>(null);
|
|
43
|
+
const [syncInfo, setSyncInfo] = useState<IGroupSyncInfo | null>(null);
|
|
27
44
|
const [loading, setLoading] = useState(true);
|
|
28
45
|
const [notFound, setNotFound] = useState(false);
|
|
46
|
+
const [downloading, setDownloading] = useState(false);
|
|
47
|
+
const [downloadProgress, setDownloadProgress] = useState<string | null>(null);
|
|
48
|
+
const [downloadResult, setDownloadResult] = useState<{
|
|
49
|
+
success: boolean;
|
|
50
|
+
message: string;
|
|
51
|
+
filePath?: string;
|
|
52
|
+
size?: number;
|
|
53
|
+
} | null>(null);
|
|
29
54
|
|
|
30
55
|
const loadData = async () => {
|
|
31
56
|
try {
|
|
@@ -35,7 +60,10 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
35
60
|
return;
|
|
36
61
|
}
|
|
37
62
|
|
|
38
|
-
const groupData = await
|
|
63
|
+
const [groupData, syncData] = await Promise.all([
|
|
64
|
+
api.getGroupDetails(groupId),
|
|
65
|
+
api.getGroupSyncStatus(groupId)
|
|
66
|
+
]);
|
|
39
67
|
|
|
40
68
|
if (!groupData) {
|
|
41
69
|
setNotFound(true);
|
|
@@ -43,6 +71,10 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
43
71
|
setGroup(groupData);
|
|
44
72
|
setNotFound(false);
|
|
45
73
|
}
|
|
74
|
+
|
|
75
|
+
if (syncData && syncData.length > 0) {
|
|
76
|
+
setSyncInfo(syncData[0]);
|
|
77
|
+
}
|
|
46
78
|
} catch (error) {
|
|
47
79
|
console.error('Error loading group details:', error);
|
|
48
80
|
setNotFound(true);
|
|
@@ -51,6 +83,40 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
51
83
|
}
|
|
52
84
|
};
|
|
53
85
|
|
|
86
|
+
const handleDownloadDatabase = async (deviceId: string) => {
|
|
87
|
+
try {
|
|
88
|
+
setDownloading(true);
|
|
89
|
+
setDownloadProgress('Preparing download...');
|
|
90
|
+
setDownloadResult(null);
|
|
91
|
+
|
|
92
|
+
const api = (window as any).electronAPI?.networkViewer;
|
|
93
|
+
if (!api?.downloadRemoteDatabase) {
|
|
94
|
+
setDownloadResult({
|
|
95
|
+
success: false,
|
|
96
|
+
message: 'Download API not available'
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setDownloadProgress('Downloading database...');
|
|
102
|
+
const result = await api.downloadRemoteDatabase(groupId, deviceId);
|
|
103
|
+
|
|
104
|
+
setDownloadResult(result);
|
|
105
|
+
if (result.success) {
|
|
106
|
+
setDownloadProgress(null);
|
|
107
|
+
}
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
console.error('Error downloading database:', error);
|
|
110
|
+
setDownloadResult({
|
|
111
|
+
success: false,
|
|
112
|
+
message: error?.message || 'Download failed'
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
setDownloading(false);
|
|
116
|
+
setDownloadProgress(null);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
54
120
|
useEffect(() => {
|
|
55
121
|
loadData();
|
|
56
122
|
}, [groupId]);
|
|
@@ -168,6 +234,83 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
168
234
|
</div>
|
|
169
235
|
</div>
|
|
170
236
|
)}
|
|
237
|
+
|
|
238
|
+
{/* Database Download Section */}
|
|
239
|
+
<div className="mb-4">
|
|
240
|
+
<h6 className="border-bottom pb-2 mb-3">
|
|
241
|
+
<i className="bi bi-download me-2"></i>
|
|
242
|
+
Download Database (Fast Sync)
|
|
243
|
+
</h6>
|
|
244
|
+
<p className="text-muted small mb-3">
|
|
245
|
+
Download the entire database from a connected device. This is useful for fast initial sync on a new device.
|
|
246
|
+
The database will be saved as a backup file.
|
|
247
|
+
</p>
|
|
248
|
+
|
|
249
|
+
{syncInfo && syncInfo.connectedDevices.length > 0 ? (
|
|
250
|
+
<div>
|
|
251
|
+
<label className="form-label">Select a connected device:</label>
|
|
252
|
+
<div className="d-flex flex-wrap gap-2">
|
|
253
|
+
{syncInfo.connectedDevices.map((device) => (
|
|
254
|
+
<button
|
|
255
|
+
key={device.deviceId}
|
|
256
|
+
className="btn btn-outline-primary btn-sm"
|
|
257
|
+
onClick={() => handleDownloadDatabase(device.deviceId)}
|
|
258
|
+
disabled={downloading}
|
|
259
|
+
>
|
|
260
|
+
{downloading ? (
|
|
261
|
+
<>
|
|
262
|
+
<span className="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
|
263
|
+
Downloading...
|
|
264
|
+
</>
|
|
265
|
+
) : (
|
|
266
|
+
<>
|
|
267
|
+
<i className="bi bi-cloud-download me-1"></i>
|
|
268
|
+
{device.deviceId.substring(0, 8)}...
|
|
269
|
+
<span className="badge bg-secondary ms-1">{Math.round(device.latencyMs)}ms</span>
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="alert alert-info mb-0">
|
|
278
|
+
<i className="bi bi-info-circle me-2"></i>
|
|
279
|
+
No devices are currently connected to this group. Connect to a device first to download its database.
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{downloadProgress && (
|
|
284
|
+
<div className="mt-3">
|
|
285
|
+
<div className="d-flex align-items-center">
|
|
286
|
+
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
287
|
+
<span>{downloadProgress}</span>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{downloadResult && (
|
|
293
|
+
<div className={`alert mt-3 mb-0 ${downloadResult.success ? 'alert-success' : 'alert-danger'}`}>
|
|
294
|
+
{downloadResult.success ? (
|
|
295
|
+
<>
|
|
296
|
+
<i className="bi bi-check-circle me-2"></i>
|
|
297
|
+
<strong>Download complete!</strong>
|
|
298
|
+
<br />
|
|
299
|
+
<small>
|
|
300
|
+
File: <code>{downloadResult.filePath}</code>
|
|
301
|
+
<br />
|
|
302
|
+
Size: {downloadResult.size ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB` : 'Unknown'}
|
|
303
|
+
</small>
|
|
304
|
+
</>
|
|
305
|
+
) : (
|
|
306
|
+
<>
|
|
307
|
+
<i className="bi bi-exclamation-circle me-2"></i>
|
|
308
|
+
<strong>Download failed:</strong> {downloadResult.message}
|
|
309
|
+
</>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
171
314
|
</>
|
|
172
315
|
)}
|
|
173
316
|
</div>
|
|
@@ -8,6 +8,25 @@ import { DeviceDetailsModal } from './device-details-modal';
|
|
|
8
8
|
import { GroupDetailsModal } from './group-details-modal';
|
|
9
9
|
import { UsageGraph } from './usage-graph';
|
|
10
10
|
|
|
11
|
+
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
12
|
+
function formatBytes(bytes: number): string {
|
|
13
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
14
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
15
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
16
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Format transfer rate to human-readable string (KB/s or MB/s) */
|
|
20
|
+
function formatRate(mbps: number | undefined): string {
|
|
21
|
+
if (mbps === undefined || mbps === 0) return '0 KB/s';
|
|
22
|
+
// Show KB/s if less than 0.1 MB/s (100 KB/s)
|
|
23
|
+
if (mbps < 0.1) {
|
|
24
|
+
const kbps = mbps * 1024;
|
|
25
|
+
return `${kbps.toFixed(1)} KB/s`;
|
|
26
|
+
}
|
|
27
|
+
return `${mbps.toFixed(2)} MB/s`;
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
// TypeScript type definitions (matching backend)
|
|
12
31
|
interface INetworkOverview {
|
|
13
32
|
localDeviceId: string;
|
|
@@ -43,6 +62,11 @@ interface IConnectionInfo {
|
|
|
43
62
|
errorRate: number;
|
|
44
63
|
isServer: boolean;
|
|
45
64
|
connectionAddress?: string;
|
|
65
|
+
// Throughput stats
|
|
66
|
+
bytesSent?: number;
|
|
67
|
+
bytesReceived?: number;
|
|
68
|
+
sendRateMBps?: number;
|
|
69
|
+
receiveRateMBps?: number;
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
interface IDeviceConnection {
|
|
@@ -329,35 +353,6 @@ export function NetworkViewerList() {
|
|
|
329
353
|
</div>
|
|
330
354
|
</div>
|
|
331
355
|
|
|
332
|
-
{/* Groups Summary */}
|
|
333
|
-
{overview.groups.length > 0 && (
|
|
334
|
-
<div className="card mb-4">
|
|
335
|
-
<div className="card-body">
|
|
336
|
-
<h5 className="card-title">Groups: {overview.groups.length}</h5>
|
|
337
|
-
<div className="table-responsive">
|
|
338
|
-
<table className="table table-sm">
|
|
339
|
-
<thead>
|
|
340
|
-
<tr>
|
|
341
|
-
<th>Group Name</th>
|
|
342
|
-
<th>Connected Devices</th>
|
|
343
|
-
<th>Total Members</th>
|
|
344
|
-
</tr>
|
|
345
|
-
</thead>
|
|
346
|
-
<tbody>
|
|
347
|
-
{overview.groups.map(group => (
|
|
348
|
-
<tr key={group.groupId}>
|
|
349
|
-
<td>{group.groupName}</td>
|
|
350
|
-
<td>{group.connectedDevices}</td>
|
|
351
|
-
<td>{group.totalMembers}</td>
|
|
352
|
-
</tr>
|
|
353
|
-
))}
|
|
354
|
-
</tbody>
|
|
355
|
-
</table>
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
)}
|
|
360
|
-
|
|
361
356
|
{/* Connections List */}
|
|
362
357
|
<div className="card mb-4">
|
|
363
358
|
<div className="card-body">
|
|
@@ -379,6 +374,7 @@ export function NetworkViewerList() {
|
|
|
379
374
|
<th>Shared Groups</th>
|
|
380
375
|
<th>Latency</th>
|
|
381
376
|
<th>Error Rate</th>
|
|
377
|
+
<th>Throughput</th>
|
|
382
378
|
{/* <th>State</th> */}
|
|
383
379
|
<th>Actions</th>
|
|
384
380
|
</tr>
|
|
@@ -451,6 +447,17 @@ export function NetworkViewerList() {
|
|
|
451
447
|
{(conn.errorRate * 100).toFixed(1)}%
|
|
452
448
|
</span>
|
|
453
449
|
</td>
|
|
450
|
+
<td>
|
|
451
|
+
<small>
|
|
452
|
+
<span className="text-success">↑{formatRate(conn.sendRateMBps)}</span>
|
|
453
|
+
{' / '}
|
|
454
|
+
<span className="text-primary">↓{formatRate(conn.receiveRateMBps)}</span>
|
|
455
|
+
</small>
|
|
456
|
+
<br />
|
|
457
|
+
<small className="text-muted">
|
|
458
|
+
{formatBytes(conn.bytesSent || 0)} / {formatBytes(conn.bytesReceived || 0)}
|
|
459
|
+
</small>
|
|
460
|
+
</td>
|
|
454
461
|
{/* <td>
|
|
455
462
|
<span className="badge bg-success">{conn.connectionState}</span>
|
|
456
463
|
</td> */}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { Tooltip } from '../../components/tooltip';
|
|
4
|
-
import { ColorModeDropdown } from './color-mode-dropdown';
|
|
1
|
+
import { Devices, getUserContext, IDevice, packagesRootDir, reloadPackagesOnPageRefresh, rpcServerCalls, thisDeviceId, TrustLevel } from "@peers-app/peers-sdk";
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
5
3
|
import { Input } from '../../components/input';
|
|
4
|
+
import { Tooltip } from '../../components/tooltip';
|
|
6
5
|
import { useObservable, useObservableState } from '../../hooks';
|
|
6
|
+
import { ColorModeDropdown } from './color-mode-dropdown';
|
|
7
7
|
|
|
8
8
|
export const SettingsPage: React.FC = () => {
|
|
9
9
|
return (
|
|
@@ -39,11 +39,19 @@ const DeviceName: React.FC = () => {
|
|
|
39
39
|
|
|
40
40
|
const handleSave = async () => {
|
|
41
41
|
try {
|
|
42
|
-
if (!deviceId)
|
|
42
|
+
if (!deviceId) {
|
|
43
|
+
console.error('No device id');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
43
46
|
const userContext = await getUserContext();
|
|
44
47
|
const devicesTable = Devices(userContext.userDataContext);
|
|
45
|
-
const device = await devicesTable.get(deviceId)
|
|
46
|
-
|
|
48
|
+
const device: IDevice = await devicesTable.get(deviceId) || {
|
|
49
|
+
deviceId,
|
|
50
|
+
userId: userContext.userId,
|
|
51
|
+
firstSeen: new Date(),
|
|
52
|
+
lastSeen: new Date(),
|
|
53
|
+
trustLevel: TrustLevel.NewDevice,
|
|
54
|
+
};
|
|
47
55
|
device.name = deviceName() || undefined;
|
|
48
56
|
await devicesTable.save(device);
|
|
49
57
|
currentDeviceName(deviceName());
|
|
@@ -69,8 +77,8 @@ const DeviceName: React.FC = () => {
|
|
|
69
77
|
/>
|
|
70
78
|
<button
|
|
71
79
|
className='btn btn-primary btn-sm'
|
|
72
|
-
onClick={handleSave}
|
|
73
|
-
disabled={currentDeviceName() === deviceName()}
|
|
80
|
+
onClick={handleSave}
|
|
81
|
+
disabled={currentDeviceName() === deviceName()}
|
|
74
82
|
>
|
|
75
83
|
Save
|
|
76
84
|
</button>
|
|
@@ -85,7 +85,7 @@ export const SetupUser = () => {
|
|
|
85
85
|
// Step 1: Select User Type
|
|
86
86
|
if (step === 'select-user-type' && isExistingUser === null) {
|
|
87
87
|
return (
|
|
88
|
-
<div className="container-fluid d-flex align-items-start justify-content-center" style={{
|
|
88
|
+
<div className="container-fluid d-flex align-items-start justify-content-center" style={{ paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' }}>
|
|
89
89
|
<div className="card shadow" style={{ maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' }}>
|
|
90
90
|
<div className="card-body p-4 p-md-5">
|
|
91
91
|
<div className="text-center mb-4">
|
|
@@ -129,7 +129,7 @@ export const SetupUser = () => {
|
|
|
129
129
|
// Step 2: New User Confirmation
|
|
130
130
|
if (isExistingUser === false) {
|
|
131
131
|
return (
|
|
132
|
-
<div className="container-fluid d-flex align-items-start justify-content-center" style={{
|
|
132
|
+
<div className="container-fluid d-flex align-items-start justify-content-center" style={{ paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' }}>
|
|
133
133
|
<div className="card shadow" style={{ maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' }}>
|
|
134
134
|
<div className="card-body p-4 p-md-5">
|
|
135
135
|
<div className="text-center mb-4">
|
|
@@ -182,12 +182,15 @@ export const SetupUser = () => {
|
|
|
182
182
|
// Step 3: Existing User Sign In
|
|
183
183
|
if (isExistingUser === true) {
|
|
184
184
|
return (
|
|
185
|
-
<div className="container-fluid d-flex align-items-start justify-content-center" style={{
|
|
185
|
+
<div className="container-fluid d-flex align-items-start justify-content-center" style={{ paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' }}>
|
|
186
186
|
<div className="card shadow" style={{ maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' }}>
|
|
187
187
|
<div className="card-body p-4 p-md-5">
|
|
188
|
-
<div className="text-center mb-
|
|
189
|
-
<
|
|
190
|
-
|
|
188
|
+
<div className="text-center mb-2">
|
|
189
|
+
<h3 className={`fw-bold mb-2 ${isDark ? 'text-light' : ''}`}>
|
|
190
|
+
<span>Sign In</span>
|
|
191
|
+
|
|
192
|
+
<i className="bi bi-box-arrow-in-right text-primary" />
|
|
193
|
+
</h3>
|
|
191
194
|
<p className={isDark ? 'text-light opacity-75' : 'text-muted'}>Enter your existing credentials</p>
|
|
192
195
|
</div>
|
|
193
196
|
|
package/src/system-apps/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { contactsApp } from './contacts.app';
|
|
|
20
20
|
export { consoleLogsApp } from './console-logs.app';
|
|
21
21
|
export { networkViewerApp } from './network-viewer.app';
|
|
22
22
|
export { dataExplorerApp } from './data-explorer.app';
|
|
23
|
+
export { mobileSettingsApp } from './mobile-settings.app';
|
|
23
24
|
|
|
24
25
|
// Import individual apps
|
|
25
26
|
import { searchApp } from './search.app';
|
|
@@ -41,6 +42,12 @@ import { contactsApp } from './contacts.app';
|
|
|
41
42
|
import { consoleLogsApp } from './console-logs.app';
|
|
42
43
|
import { networkViewerApp } from './network-viewer.app';
|
|
43
44
|
import { dataExplorerApp } from './data-explorer.app';
|
|
45
|
+
import { mobileSettingsApp } from './mobile-settings.app';
|
|
46
|
+
|
|
47
|
+
// Helper to check if running in React Native
|
|
48
|
+
function isReactNative(): boolean {
|
|
49
|
+
return typeof (window as any).__NATIVE_THEME !== 'undefined';
|
|
50
|
+
}
|
|
44
51
|
|
|
45
52
|
// Collection of all system apps
|
|
46
53
|
export const systemApps: IAppNav[] = [
|
|
@@ -67,6 +74,8 @@ export const systemApps: IAppNav[] = [
|
|
|
67
74
|
// User & Settings Apps
|
|
68
75
|
profileApp,
|
|
69
76
|
settingsApp,
|
|
77
|
+
// Mobile Settings (only in React Native)
|
|
78
|
+
...(isReactNative() ? [mobileSettingsApp] : []),
|
|
70
79
|
|
|
71
80
|
// System Tools & Debugging
|
|
72
81
|
consoleLogsApp,
|
|
@@ -133,7 +133,7 @@ function TabsLayoutInternal() {
|
|
|
133
133
|
) : (
|
|
134
134
|
<div className="d-flex align-items-center px-1" style={{ height: '36px' }}>
|
|
135
135
|
{/* Group Switcher */}
|
|
136
|
-
<GroupSwitcher colorMode={_colorMode} />
|
|
136
|
+
<GroupSwitcher colorMode={_colorMode} isMobile={false} />
|
|
137
137
|
|
|
138
138
|
{/* Search Button */}
|
|
139
139
|
<button
|
|
@@ -207,112 +207,122 @@ interface MobileTabsHeaderProps {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode }: MobileTabsHeaderProps) {
|
|
210
|
-
const [
|
|
210
|
+
const [showMenuDropdown, setShowMenuDropdown] = React.useState(false);
|
|
211
211
|
const currentTab = tabs.find(t => t.tabId === activeTab);
|
|
212
212
|
const nonLauncherTabs = tabs.filter(t => t.packageId !== 'launcher');
|
|
213
213
|
|
|
214
214
|
return (
|
|
215
|
-
<div className="d-flex align-items-center justify-content-between px-2" style={{ height: '36px' }}>
|
|
216
|
-
{/* Left Side - Group Switcher
|
|
215
|
+
<div className="d-flex align-items-center justify-content-between px-2 position-relative" style={{ height: '36px' }}>
|
|
216
|
+
{/* Left Side - Group Switcher */}
|
|
217
217
|
<div className="d-flex align-items-center gap-1">
|
|
218
|
-
<GroupSwitcher colorMode={colorMode} />
|
|
219
|
-
<button
|
|
220
|
-
className="btn btn-sm"
|
|
221
|
-
onClick={openCommandPalette}
|
|
222
|
-
title="Search everything"
|
|
223
|
-
style={{
|
|
224
|
-
minWidth: '36px',
|
|
225
|
-
border: 'none',
|
|
226
|
-
background: 'transparent',
|
|
227
|
-
color: colorMode === 'light' ? '#6c757d' : '#adb5bd'
|
|
228
|
-
}}
|
|
229
|
-
onMouseEnter={(e) => {
|
|
230
|
-
e.currentTarget.style.backgroundColor = colorMode === 'light' ? '#f8f9fa' : '#495057';
|
|
231
|
-
e.currentTarget.style.color = colorMode === 'light' ? '#0d6efd' : '#ffffff';
|
|
232
|
-
}}
|
|
233
|
-
onMouseLeave={(e) => {
|
|
234
|
-
e.currentTarget.style.backgroundColor = 'transparent';
|
|
235
|
-
e.currentTarget.style.color = colorMode === 'light' ? '#6c757d' : '#adb5bd';
|
|
236
|
-
}}
|
|
237
|
-
>
|
|
238
|
-
<i className="bi-search" />
|
|
239
|
-
</button>
|
|
240
|
-
<button
|
|
241
|
-
className={`btn btn-sm ${colorMode === 'light' ? 'btn-outline-primary' : 'btn-outline-light'}`}
|
|
242
|
-
onClick={() => onSwitch('launcher')}
|
|
243
|
-
style={{ minWidth: '36px' }}
|
|
244
|
-
>
|
|
245
|
-
<i className="bi-grid-3x3-gap" />
|
|
246
|
-
</button>
|
|
218
|
+
<GroupSwitcher colorMode={colorMode} isMobile={true} />
|
|
247
219
|
</div>
|
|
248
220
|
|
|
249
|
-
{/* Center - Current Tab Display */}
|
|
250
|
-
<div
|
|
221
|
+
{/* Center - Current Tab Display (absolutely positioned to center on screen) */}
|
|
222
|
+
<div
|
|
223
|
+
className="d-flex align-items-center justify-content-center position-absolute"
|
|
224
|
+
style={{
|
|
225
|
+
left: '50%',
|
|
226
|
+
transform: 'translateX(-50%)',
|
|
227
|
+
pointerEvents: 'none',
|
|
228
|
+
maxWidth: 'calc(100% - 140px)'
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
251
231
|
{currentTab?.iconClassName && currentTab.packageId !== 'launcher' && (
|
|
252
232
|
<i className={`${currentTab.iconClassName} me-2`} />
|
|
253
233
|
)}
|
|
254
|
-
<span className="fw-medium text-truncate
|
|
234
|
+
<span className="fw-medium text-truncate small">
|
|
255
235
|
{currentTab?.title || 'Apps'}
|
|
256
236
|
</span>
|
|
257
237
|
</div>
|
|
258
238
|
|
|
259
|
-
{/* Right Side -
|
|
260
|
-
<div className="d-flex align-items-center
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
239
|
+
{/* Right Side - Hamburger Menu */}
|
|
240
|
+
<div className="d-flex align-items-center">
|
|
241
|
+
<div className="dropdown">
|
|
242
|
+
<button
|
|
243
|
+
className={`btn btn-sm ${colorMode === 'light' ? 'btn-outline-dark' : 'btn-outline-light'}`}
|
|
244
|
+
onClick={() => setShowMenuDropdown(!showMenuDropdown)}
|
|
245
|
+
style={{ minWidth: '36px' }}
|
|
246
|
+
>
|
|
247
|
+
<i className="bi-list" />
|
|
248
|
+
{nonLauncherTabs.length > 0 && (
|
|
269
249
|
<span className="ms-1">{nonLauncherTabs.length}</span>
|
|
270
|
-
|
|
271
|
-
|
|
250
|
+
)}
|
|
251
|
+
</button>
|
|
252
|
+
{showMenuDropdown && (
|
|
253
|
+
<div
|
|
254
|
+
className={`dropdown-menu show position-absolute ${colorMode === 'light' ? '' : 'dropdown-menu-dark'}`}
|
|
255
|
+
style={{ right: 0, top: '100%', zIndex: 1000, minWidth: '250px' }}
|
|
256
|
+
>
|
|
257
|
+
{/* Apps Option */}
|
|
272
258
|
<div
|
|
273
|
-
className={`dropdown-
|
|
274
|
-
style={{
|
|
259
|
+
className={`dropdown-item d-flex align-items-center ${activeTab === 'launcher' ? 'active' : ''}`}
|
|
260
|
+
style={{ cursor: 'pointer' }}
|
|
261
|
+
onClick={() => {
|
|
262
|
+
onSwitch('launcher');
|
|
263
|
+
setShowMenuDropdown(false);
|
|
264
|
+
}}
|
|
275
265
|
>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
266
|
+
<i className="bi-grid-3x3-gap me-2" />
|
|
267
|
+
<span>Apps</span>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Search Option */}
|
|
271
|
+
<div
|
|
272
|
+
className="dropdown-item d-flex align-items-center"
|
|
273
|
+
style={{ cursor: 'pointer' }}
|
|
274
|
+
onClick={() => {
|
|
275
|
+
openCommandPalette();
|
|
276
|
+
setShowMenuDropdown(false);
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<i className="bi-search me-2" />
|
|
280
|
+
<span>Search</span>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Divider if there are open tabs */}
|
|
284
|
+
{nonLauncherTabs.length > 0 && (
|
|
285
|
+
<div className="dropdown-divider" />
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* Open Tabs */}
|
|
289
|
+
{nonLauncherTabs.slice().reverse().map(tab => (
|
|
290
|
+
<div
|
|
291
|
+
key={tab.tabId}
|
|
292
|
+
className={`dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? 'active' : ''}`}
|
|
293
|
+
style={{ cursor: 'pointer' }}
|
|
294
|
+
onClick={() => {
|
|
295
|
+
onSwitch(tab.tabId);
|
|
296
|
+
setShowMenuDropdown(false);
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<div className="d-flex align-items-center">
|
|
300
|
+
{tab.iconClassName && <i className={`${tab.iconClassName} me-2`} />}
|
|
301
|
+
<span>{tab.title}</span>
|
|
302
|
+
</div>
|
|
303
|
+
<button
|
|
304
|
+
className="btn btn-sm p-0 ms-2"
|
|
305
|
+
style={{ width: '20px', height: '20px' }}
|
|
306
|
+
onClick={(e) => {
|
|
307
|
+
e.stopPropagation();
|
|
308
|
+
onClose(tab.tabId);
|
|
284
309
|
}}
|
|
285
310
|
>
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
style={{ width: '20px', height: '20px' }}
|
|
294
|
-
onClick={(e) => {
|
|
295
|
-
e.stopPropagation();
|
|
296
|
-
onClose(tab.tabId);
|
|
297
|
-
}}
|
|
298
|
-
>
|
|
299
|
-
<i className="bi-x" />
|
|
300
|
-
</button>
|
|
301
|
-
)}
|
|
302
|
-
</div>
|
|
303
|
-
))}
|
|
304
|
-
</div>
|
|
305
|
-
)}
|
|
306
|
-
</div>
|
|
307
|
-
)}
|
|
311
|
+
<i className="bi-x" />
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
308
318
|
</div>
|
|
309
319
|
|
|
310
320
|
{/* Backdrop to close dropdown */}
|
|
311
|
-
{
|
|
321
|
+
{showMenuDropdown && (
|
|
312
322
|
<div
|
|
313
323
|
className="position-fixed w-100 h-100"
|
|
314
324
|
style={{ top: 0, left: 0, zIndex: 999 }}
|
|
315
|
-
onClick={() =>
|
|
325
|
+
onClick={() => setShowMenuDropdown(false)}
|
|
316
326
|
/>
|
|
317
327
|
)}
|
|
318
328
|
</div>
|
|
@@ -515,6 +525,22 @@ function AppLauncherTab({ isMobile }: AppLauncherTabProps) {
|
|
|
515
525
|
.filter(Boolean) as AppItem[];
|
|
516
526
|
|
|
517
527
|
const openApp = (appItem: AppItem) => {
|
|
528
|
+
// Check if this is the mobile-settings app and we're in React Native
|
|
529
|
+
if (appItem.path === 'mobile-settings' && typeof (window as any).__NATIVE_THEME !== 'undefined') {
|
|
530
|
+
// Use expo-linking to navigate to native screen
|
|
531
|
+
// @ts-ignore
|
|
532
|
+
if (window.ReactNativeWebView) {
|
|
533
|
+
// @ts-ignore
|
|
534
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
535
|
+
type: 'navigate',
|
|
536
|
+
path: 'mobile-settings'
|
|
537
|
+
}));
|
|
538
|
+
} else {
|
|
539
|
+
// Fallback to deep link
|
|
540
|
+
window.location.href = 'peers://mobile-settings';
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
518
544
|
goToTabPath(appItem.path);
|
|
519
545
|
};
|
|
520
546
|
|