adb-webui 1.0.0

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/index.html ADDED
@@ -0,0 +1,1132 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ADB APK Installer — Android Device Manager</title>
7
+ <meta name="description" content="A beautiful, powerful ADB APK installer and Android device manager. Install APKs, manage packages, and control your Android device via ADB.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --bg: #0a0a0f;
14
+ --bg-card: #111118;
15
+ --bg-card2: #16161f;
16
+ --bg-hover: #1c1c28;
17
+ --border: rgba(255,255,255,0.07);
18
+ --border-bright: rgba(255,255,255,0.14);
19
+ --accent: #6c63ff;
20
+ --accent2: #a78bfa;
21
+ --green: #22c55e;
22
+ --red: #ef4444;
23
+ --yellow: #f59e0b;
24
+ --cyan: #06b6d4;
25
+ --text: #e2e8f0;
26
+ --text-muted: #64748b;
27
+ --text-dim: #94a3b8;
28
+ --radius: 14px;
29
+ --radius-sm: 8px;
30
+ --shadow: 0 4px 32px rgba(0,0,0,0.5);
31
+ --glow: 0 0 40px rgba(108,99,255,0.15);
32
+ }
33
+
34
+ * { box-sizing: border-box; margin: 0; padding: 0; }
35
+
36
+ body {
37
+ background: var(--bg);
38
+ color: var(--text);
39
+ font-family: 'Inter', sans-serif;
40
+ min-height: 100vh;
41
+ overflow-x: hidden;
42
+ }
43
+
44
+ /* ─── Background ─── */
45
+ body::before {
46
+ content: '';
47
+ position: fixed; inset: 0;
48
+ background:
49
+ radial-gradient(ellipse 80% 50% at 20% 0%, rgba(108,99,255,0.12) 0%, transparent 60%),
50
+ radial-gradient(ellipse 60% 40% at 80% 100%, rgba(6,182,212,0.08) 0%, transparent 60%);
51
+ pointer-events: none; z-index: 0;
52
+ }
53
+
54
+ /* ─── Layout ─── */
55
+ .app { position: relative; z-index: 1; max-width: 1400px; margin: 0 auto; padding: 0 24px 80px; }
56
+
57
+ /* ─── Header ─── */
58
+ header {
59
+ display: flex; align-items: center; justify-content: space-between;
60
+ padding: 28px 0 32px;
61
+ border-bottom: 1px solid var(--border);
62
+ margin-bottom: 32px;
63
+ }
64
+ .logo { display: flex; align-items: center; gap: 14px; }
65
+ .logo-icon {
66
+ width: 46px; height: 46px;
67
+ background: linear-gradient(135deg, var(--accent), var(--cyan));
68
+ border-radius: 12px;
69
+ display: flex; align-items: center; justify-content: center;
70
+ font-size: 22px;
71
+ box-shadow: 0 0 24px rgba(108,99,255,0.4);
72
+ }
73
+ .logo-text h1 { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; }
74
+ .logo-text span { font-size: 12px; color: var(--text-muted); font-weight: 400; }
75
+ .header-status { display: flex; align-items: center; gap: 10px; }
76
+ .status-dot {
77
+ width: 8px; height: 8px; border-radius: 50%;
78
+ background: var(--text-muted);
79
+ transition: background .3s, box-shadow .3s;
80
+ }
81
+ .status-dot.connected { background: var(--green); box-shadow: 0 0 8px var(--green); }
82
+ .status-dot.error { background: var(--red); box-shadow: 0 0 8px var(--red); }
83
+ #adb-status-text { font-size: 13px; color: var(--text-muted); }
84
+
85
+ /* ─── Grid ─── */
86
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
87
+ .grid-3 { grid-template-columns: 1fr 1fr 1fr; }
88
+ @media (max-width: 900px) { .grid, .grid-3 { grid-template-columns: 1fr; } }
89
+
90
+ /* ─── Card ─── */
91
+ .card {
92
+ background: var(--bg-card);
93
+ border: 1px solid var(--border);
94
+ border-radius: var(--radius);
95
+ padding: 24px;
96
+ transition: border-color .2s, box-shadow .2s;
97
+ }
98
+ .card:hover { border-color: var(--border-bright); }
99
+ .card-header {
100
+ display: flex; align-items: center; justify-content: space-between;
101
+ margin-bottom: 20px;
102
+ }
103
+ .card-title {
104
+ display: flex; align-items: center; gap: 10px;
105
+ font-size: 14px; font-weight: 600; color: var(--text-dim);
106
+ text-transform: uppercase; letter-spacing: 0.05em;
107
+ }
108
+ .card-title .icon { font-size: 16px; }
109
+
110
+ /* ─── Buttons ─── */
111
+ .btn {
112
+ display: inline-flex; align-items: center; gap: 7px;
113
+ padding: 9px 18px; border-radius: var(--radius-sm);
114
+ font-size: 13px; font-weight: 500; font-family: 'Inter', sans-serif;
115
+ cursor: pointer; border: none; transition: all .15s ease;
116
+ text-decoration: none;
117
+ }
118
+ .btn:active { transform: scale(0.97); }
119
+ .btn-primary {
120
+ background: linear-gradient(135deg, var(--accent), #7c3aed);
121
+ color: #fff;
122
+ box-shadow: 0 4px 16px rgba(108,99,255,0.35);
123
+ }
124
+ .btn-primary:hover { box-shadow: 0 4px 24px rgba(108,99,255,0.55); filter: brightness(1.1); }
125
+ .btn-secondary {
126
+ background: var(--bg-hover); color: var(--text-dim);
127
+ border: 1px solid var(--border);
128
+ }
129
+ .btn-secondary:hover { background: var(--bg-card2); border-color: var(--border-bright); color: var(--text); }
130
+ .btn-danger { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
131
+ .btn-danger:hover { background: rgba(239,68,68,0.25); }
132
+ .btn-green { background: rgba(34,197,94,0.15); color: var(--green); border: 1px solid rgba(34,197,94,0.3); }
133
+ .btn-green:hover { background: rgba(34,197,94,0.25); }
134
+ .btn-sm { padding: 6px 12px; font-size: 12px; }
135
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
136
+
137
+ /* ─── Devices ─── */
138
+ .device-list { display: flex; flex-direction: column; gap: 10px; }
139
+ .device-item {
140
+ display: flex; align-items: center; gap: 14px;
141
+ padding: 14px 16px;
142
+ background: var(--bg-card2); border: 1px solid var(--border);
143
+ border-radius: var(--radius-sm);
144
+ cursor: pointer; transition: all .15s;
145
+ }
146
+ .device-item:hover { border-color: var(--accent); background: var(--bg-hover); }
147
+ .device-item.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
148
+ .device-icon { font-size: 24px; }
149
+ .device-info { flex: 1; }
150
+ .device-name { font-size: 14px; font-weight: 600; }
151
+ .device-serial { font-size: 11px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
152
+ .device-state {
153
+ font-size: 11px; padding: 2px 8px; border-radius: 99px; font-weight: 500;
154
+ }
155
+ .device-state.online { background: rgba(34,197,94,0.15); color: var(--green); }
156
+ .device-state.offline { background: rgba(239,68,68,0.15); color: var(--red); }
157
+ .device-state.unauthorized { background: rgba(245,158,11,0.15); color: var(--yellow); }
158
+ .no-devices {
159
+ text-align: center; padding: 32px;
160
+ color: var(--text-muted); font-size: 13px;
161
+ }
162
+ .no-devices .big { font-size: 36px; margin-bottom: 8px; }
163
+
164
+ /* ─── Drop zone ─── */
165
+ #drop-zone {
166
+ border: 2px dashed var(--border-bright);
167
+ border-radius: var(--radius);
168
+ padding: 40px;
169
+ text-align: center;
170
+ cursor: pointer;
171
+ transition: all .2s;
172
+ position: relative;
173
+ background: rgba(108,99,255,0.03);
174
+ }
175
+ #drop-zone:hover, #drop-zone.dragging {
176
+ border-color: var(--accent);
177
+ background: rgba(108,99,255,0.08);
178
+ box-shadow: 0 0 24px rgba(108,99,255,0.15);
179
+ }
180
+ #drop-zone .drop-icon { font-size: 44px; margin-bottom: 12px; line-height: 1; }
181
+ #drop-zone h3 { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
182
+ #drop-zone p { font-size: 13px; color: var(--text-muted); }
183
+ #file-input { display: none; }
184
+ .upload-progress {
185
+ width: 100%; height: 4px;
186
+ background: var(--border);
187
+ border-radius: 2px; margin-top: 16px; overflow: hidden;
188
+ display: none;
189
+ }
190
+ .upload-progress-bar {
191
+ height: 100%; width: 0;
192
+ background: linear-gradient(90deg, var(--accent), var(--cyan));
193
+ border-radius: 2px;
194
+ transition: width .2s;
195
+ animation: shimmer 1.5s infinite;
196
+ }
197
+ @keyframes shimmer {
198
+ 0% { filter: brightness(1); }
199
+ 50% { filter: brightness(1.3); }
200
+ 100% { filter: brightness(1); }
201
+ }
202
+
203
+ /* ─── APK List ─── */
204
+ .apk-list { display: flex; flex-direction: column; gap: 8px; max-height: 320px; overflow-y: auto; }
205
+ .apk-list::-webkit-scrollbar { width: 4px; }
206
+ .apk-list::-webkit-scrollbar-track { background: transparent; }
207
+ .apk-list::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
208
+ .apk-item {
209
+ display: flex; align-items: center; gap: 12px;
210
+ padding: 12px 14px;
211
+ background: var(--bg-card2); border: 1px solid var(--border);
212
+ border-radius: var(--radius-sm);
213
+ transition: all .15s;
214
+ }
215
+ .apk-item:hover { border-color: var(--border-bright); }
216
+ .apk-item.selected { border-color: var(--accent); background: rgba(108,99,255,0.08); }
217
+ .apk-icon { font-size: 20px; flex-shrink: 0; }
218
+ .apk-info { flex: 1; min-width: 0; }
219
+ .apk-name {
220
+ font-size: 13px; font-weight: 500;
221
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
222
+ }
223
+ .apk-size { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
224
+ .apk-actions { display: flex; gap: 6px; flex-shrink: 0; }
225
+
226
+ /* ─── Install Panel ─── */
227
+ .install-panel { margin-top: 20px; }
228
+ .install-options { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
229
+ .checkbox-wrap {
230
+ display: flex; align-items: center; gap: 8px;
231
+ cursor: pointer; font-size: 13px; color: var(--text-dim);
232
+ user-select: none;
233
+ }
234
+ .checkbox-wrap input[type=checkbox] {
235
+ width: 16px; height: 16px;
236
+ accent-color: var(--accent);
237
+ cursor: pointer;
238
+ }
239
+ .checkbox-wrap:hover { color: var(--text); }
240
+
241
+ /* ─── Log Console ─── */
242
+ .log-console {
243
+ background: #08080d;
244
+ border: 1px solid var(--border);
245
+ border-radius: var(--radius);
246
+ padding: 0;
247
+ overflow: hidden;
248
+ }
249
+ .log-console-header {
250
+ display: flex; align-items: center; justify-content: space-between;
251
+ padding: 12px 16px;
252
+ border-bottom: 1px solid var(--border);
253
+ background: var(--bg-card);
254
+ }
255
+ .log-console-header .title { font-size: 13px; font-weight: 600; color: var(--text-dim); display: flex; align-items: center; gap: 8px; }
256
+ .live-dot {
257
+ width: 6px; height: 6px; border-radius: 50%;
258
+ background: var(--green);
259
+ animation: pulse 1.5s infinite;
260
+ }
261
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
262
+ #log-output {
263
+ font-family: 'JetBrains Mono', monospace;
264
+ font-size: 12px; line-height: 1.7;
265
+ padding: 16px;
266
+ height: 280px;
267
+ overflow-y: auto;
268
+ word-break: break-all;
269
+ }
270
+ #log-output::-webkit-scrollbar { width: 4px; }
271
+ #log-output::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
272
+ .log-line { display: flex; gap: 12px; padding: 1px 0; }
273
+ .log-time { color: var(--text-muted); flex-shrink: 0; }
274
+ .log-msg.info { color: #94a3b8; }
275
+ .log-msg.success { color: var(--green); }
276
+ .log-msg.error { color: var(--red); }
277
+ .log-msg.warn { color: var(--yellow); }
278
+ .log-msg.stdout { color: #a78bfa; }
279
+ .log-msg.stderr { color: #fb923c; }
280
+ .log-empty { color: var(--text-muted); font-style: italic; }
281
+
282
+ /* ─── Quick Actions ─── */
283
+ .quick-actions { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
284
+ .action-btn {
285
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
286
+ padding: 16px 12px;
287
+ background: var(--bg-card2); border: 1px solid var(--border);
288
+ border-radius: var(--radius-sm);
289
+ cursor: pointer; transition: all .15s;
290
+ font-family: 'Inter', sans-serif;
291
+ font-size: 12px; font-weight: 500; color: var(--text-dim);
292
+ }
293
+ .action-btn:hover { border-color: var(--border-bright); color: var(--text); background: var(--bg-hover); }
294
+ .action-btn .a-icon { font-size: 22px; }
295
+
296
+ /* ─── Input ─── */
297
+ .input-group { display: flex; gap: 8px; align-items: center; }
298
+ input[type=text], input[type=password] {
299
+ background: var(--bg-card2); border: 1px solid var(--border);
300
+ border-radius: var(--radius-sm);
301
+ color: var(--text); padding: 9px 14px;
302
+ font-size: 13px; font-family: 'Inter', sans-serif;
303
+ outline: none; width: 100%;
304
+ transition: border-color .15s;
305
+ }
306
+ input[type=text]:focus, input[type=password]:focus {
307
+ border-color: var(--accent);
308
+ box-shadow: 0 0 0 3px rgba(108,99,255,0.15);
309
+ }
310
+ .form-row { margin-bottom: 14px; }
311
+ .form-label { font-size: 12px; color: var(--text-muted); margin-bottom: 6px; display: block; }
312
+
313
+ /* ─── Badge ─── */
314
+ .badge {
315
+ font-size: 10px; font-weight: 600; padding: 2px 8px;
316
+ border-radius: 99px; text-transform: uppercase; letter-spacing: 0.05em;
317
+ }
318
+ .badge-accent { background: rgba(108,99,255,0.2); color: var(--accent2); }
319
+ .badge-green { background: rgba(34,197,94,0.15); color: var(--green); }
320
+
321
+ /* ─── Tabs ─── */
322
+ .tabs { display: flex; gap: 4px; margin-bottom: 20px; }
323
+ .tab {
324
+ padding: 8px 16px; border-radius: var(--radius-sm);
325
+ font-size: 13px; font-weight: 500;
326
+ cursor: pointer; border: 1px solid transparent;
327
+ color: var(--text-muted); background: transparent;
328
+ transition: all .15s; font-family: 'Inter', sans-serif;
329
+ }
330
+ .tab.active { background: var(--bg-card2); border-color: var(--border); color: var(--text); }
331
+ .tab:hover:not(.active) { color: var(--text-dim); }
332
+ .tab-panel { display: none; }
333
+ .tab-panel.active { display: block; }
334
+
335
+ /* ─── Notifications ─── */
336
+ #toast-container {
337
+ position: fixed; top: 24px; right: 24px;
338
+ display: flex; flex-direction: column; gap: 10px;
339
+ z-index: 9999;
340
+ }
341
+ .toast {
342
+ display: flex; align-items: center; gap: 12px;
343
+ padding: 14px 18px;
344
+ background: var(--bg-card); border: 1px solid var(--border);
345
+ border-radius: var(--radius-sm);
346
+ box-shadow: var(--shadow);
347
+ min-width: 280px; max-width: 380px;
348
+ animation: slideIn .25s ease forwards;
349
+ font-size: 13px;
350
+ }
351
+ .toast.success { border-color: rgba(34,197,94,0.4); }
352
+ .toast.error { border-color: rgba(239,68,68,0.4); }
353
+ @keyframes slideIn { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
354
+ @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
355
+ .toast.out { animation: slideOut .25s ease forwards; }
356
+
357
+ /* ─── Uninstall Panel ─── */
358
+ .packages-list { max-height: 240px; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 11.5px; }
359
+ .package-item {
360
+ padding: 8px 10px; border-radius: 6px;
361
+ display: flex; align-items: center; justify-content: space-between;
362
+ transition: background .1s;
363
+ }
364
+ .package-item:hover { background: var(--bg-hover); }
365
+
366
+ /* ─── Config ─── */
367
+ .config-section { padding: 20px; background: var(--bg-card2); border-radius: var(--radius-sm); border: 1px solid var(--border); }
368
+
369
+ /* ─── Divider ─── */
370
+ hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
371
+
372
+ /* ─── Scrollable full section ─── */
373
+ .section-full { margin-bottom: 20px; }
374
+
375
+ /* ─── Spinner ─── */
376
+ .spinner {
377
+ width: 16px; height: 16px;
378
+ border: 2px solid rgba(255,255,255,0.2);
379
+ border-top-color: #fff;
380
+ border-radius: 50%;
381
+ animation: spin .6s linear infinite;
382
+ display: inline-block;
383
+ }
384
+ @keyframes spin { to { transform: rotate(360deg); } }
385
+
386
+ /* ─── Confirm Modal ─── */
387
+ #confirm-overlay {
388
+ display: none;
389
+ position: fixed; inset: 0; z-index: 99999;
390
+ background: rgba(0,0,0,0.65);
391
+ backdrop-filter: blur(6px);
392
+ align-items: center; justify-content: center;
393
+ }
394
+ #confirm-overlay.open { display: flex; animation: fadeIn .15s ease; }
395
+ @keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
396
+ #confirm-box {
397
+ background: var(--bg-card);
398
+ border: 1px solid var(--border-bright);
399
+ border-radius: 18px;
400
+ padding: 32px 28px 24px;
401
+ width: 360px; max-width: 90vw;
402
+ box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04);
403
+ animation: popIn .18s cubic-bezier(.34,1.56,.64,1);
404
+ text-align: center;
405
+ }
406
+ @keyframes popIn { from { transform: scale(0.88); opacity:0; } to { transform: scale(1); opacity:1; } }
407
+ #confirm-icon { font-size: 40px; margin-bottom: 12px; line-height: 1; }
408
+ #confirm-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; color: var(--text); }
409
+ #confirm-msg { font-size: 13px; color: var(--text-muted); line-height: 1.6; margin-bottom: 24px; }
410
+ .confirm-actions { display: flex; gap: 10px; justify-content: center; }
411
+ #confirm-cancel {
412
+ flex: 1; padding: 10px 0; border-radius: var(--radius-sm);
413
+ background: var(--bg-hover); color: var(--text-dim);
414
+ border: 1px solid var(--border); font-size: 14px; font-weight: 500;
415
+ font-family: 'Inter', sans-serif; cursor: pointer; transition: all .15s;
416
+ }
417
+ #confirm-cancel:hover { background: var(--bg-card2); color: var(--text); }
418
+ #confirm-ok {
419
+ flex: 1; padding: 10px 0; border-radius: var(--radius-sm);
420
+ font-size: 14px; font-weight: 600;
421
+ font-family: 'Inter', sans-serif; cursor: pointer;
422
+ border: none; transition: all .15s;
423
+ }
424
+ #confirm-ok.danger { background: var(--red); color: #fff; box-shadow: 0 4px 14px rgba(239,68,68,0.4); }
425
+ #confirm-ok.danger:hover { filter: brightness(1.1); }
426
+ #confirm-ok.primary { background: linear-gradient(135deg, var(--accent), #7c3aed); color: #fff; box-shadow: 0 4px 14px rgba(108,99,255,0.4); }
427
+ #confirm-ok.primary:hover { filter: brightness(1.1); }
428
+ </style>
429
+ </head>
430
+ <body>
431
+ <div class="app">
432
+
433
+ <!-- Header -->
434
+ <header>
435
+ <div class="logo">
436
+ <div class="logo-icon">📱</div>
437
+ <div class="logo-text">
438
+ <h1>ADB Installer</h1>
439
+ <span>Android Device Manager</span>
440
+ </div>
441
+ </div>
442
+ <div class="header-status">
443
+ <div class="status-dot" id="adb-dot"></div>
444
+ <span id="adb-status-text">Checking ADB…</span>
445
+ <button class="btn btn-secondary btn-sm" onclick="refreshAll()">⟳ Refresh</button>
446
+ </div>
447
+ </header>
448
+
449
+ <!-- Tabs -->
450
+ <div class="tabs">
451
+ <button class="tab active" onclick="switchTab('install')" id="tab-install">📦 Install APK</button>
452
+ <button class="tab" onclick="switchTab('manage')" id="tab-manage">🗂️ Manage Packages</button>
453
+ <button class="tab" onclick="switchTab('actions')" id="tab-actions">⚡ Quick Actions</button>
454
+ <button class="tab" onclick="switchTab('settings')" id="tab-settings">⚙️ Settings</button>
455
+ </div>
456
+
457
+ <!-- ══════════════════ TAB: INSTALL ══════════════════ -->
458
+ <div class="tab-panel active" id="panel-install">
459
+ <div class="grid">
460
+
461
+ <!-- Left: Devices -->
462
+ <div class="card">
463
+ <div class="card-header">
464
+ <div class="card-title"><span class="icon">📱</span> Connected Devices</div>
465
+ <button class="btn btn-secondary btn-sm" onclick="loadDevices()" id="btn-refresh-devices">Refresh</button>
466
+ </div>
467
+ <div id="device-list" class="device-list">
468
+ <div class="no-devices">
469
+ <div class="big">📡</div>
470
+ <div>Scanning for devices…</div>
471
+ </div>
472
+ </div>
473
+ <div style="margin-top:14px; padding-top:14px; border-top:1px solid var(--border)">
474
+ <div class="form-label">Selected Device</div>
475
+ <div style="font-family:'JetBrains Mono',monospace; font-size:13px; color:var(--accent2)" id="selected-serial-display">None</div>
476
+ </div>
477
+ </div>
478
+
479
+ <!-- Right: Upload -->
480
+ <div class="card">
481
+ <div class="card-header">
482
+ <div class="card-title"><span class="icon">☁️</span> Upload APK</div>
483
+ <span class="badge badge-accent" id="apk-count-badge">0 APKs</span>
484
+ </div>
485
+ <div id="drop-zone" onclick="document.getElementById('file-input').click()">
486
+ <div class="drop-icon">📦</div>
487
+ <h3>Drop APK file here</h3>
488
+ <p>or click to browse — only .apk files accepted</p>
489
+ <div class="upload-progress" id="upload-progress">
490
+ <div class="upload-progress-bar" id="upload-bar"></div>
491
+ </div>
492
+ </div>
493
+ <input type="file" id="file-input" accept=".apk">
494
+ </div>
495
+ </div>
496
+
497
+ <!-- APK Library + Install Panel -->
498
+ <div class="card section-full">
499
+ <div class="card-header">
500
+ <div class="card-title"><span class="icon">📂</span> APK Library</div>
501
+ <button class="btn btn-secondary btn-sm" onclick="loadApks()">Refresh</button>
502
+ </div>
503
+ <div id="apk-list" class="apk-list">
504
+ <div class="log-empty">No APKs uploaded yet.</div>
505
+ </div>
506
+
507
+ <div class="install-panel" id="install-panel" style="display:none">
508
+ <hr>
509
+ <div style="margin-bottom:14px; font-size:13px; color:var(--text-dim)">
510
+ Installing: <span id="selected-apk-name" style="color:var(--accent2); font-weight:600"></span>
511
+ </div>
512
+ <div class="install-options">
513
+ <label class="checkbox-wrap"><input type="checkbox" id="opt-replace" checked> Replace existing</label>
514
+ <label class="checkbox-wrap"><input type="checkbox" id="opt-grant"> Grant all permissions</label>
515
+ <label class="checkbox-wrap"><input type="checkbox" id="opt-downgrade"> Allow downgrade</label>
516
+ <label class="checkbox-wrap"><input type="checkbox" id="opt-test"> Allow test APK</label>
517
+ </div>
518
+ <div style="display:flex; gap:10px">
519
+ <button class="btn btn-primary" onclick="installSelected()" id="btn-install">
520
+ <span>🚀</span> Install APK
521
+ </button>
522
+ <button class="btn btn-secondary" onclick="clearInstallSelection()">Clear</button>
523
+ </div>
524
+ </div>
525
+ </div>
526
+
527
+ <!-- Log Console -->
528
+ <div class="log-console">
529
+ <div class="log-console-header">
530
+ <div class="title"><div class="live-dot"></div> Live Log</div>
531
+ <button class="btn btn-secondary btn-sm" onclick="clearLog()">Clear</button>
532
+ </div>
533
+ <div id="log-output">
534
+ <div class="log-empty">Waiting for events…</div>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <!-- ══════════════════ TAB: MANAGE ══════════════════ -->
540
+ <div class="tab-panel" id="panel-manage">
541
+ <div class="grid">
542
+ <div class="card">
543
+ <div class="card-header">
544
+ <div class="card-title"><span class="icon">📋</span> Installed Packages</div>
545
+ <button class="btn btn-secondary btn-sm" onclick="loadPackages()">Load</button>
546
+ </div>
547
+ <div style="margin-bottom:12px">
548
+ <input type="text" id="pkg-search" placeholder="Search packages…" oninput="filterPackages()">
549
+ </div>
550
+ <div id="packages-list" class="packages-list">
551
+ <div class="log-empty" style="padding:16px">Click "Load" to fetch installed packages.</div>
552
+ </div>
553
+ </div>
554
+
555
+ <div class="card">
556
+ <div class="card-header">
557
+ <div class="card-title"><span class="icon">🗑️</span> Uninstall Package</div>
558
+ </div>
559
+ <div class="form-row">
560
+ <div class="form-label">Package Name</div>
561
+ <input type="text" id="uninstall-pkg" placeholder="com.example.app">
562
+ </div>
563
+ <label class="checkbox-wrap" style="margin-bottom:14px">
564
+ <input type="checkbox" id="uninstall-keep-data"> Keep app data &amp; cache
565
+ </label>
566
+ <button class="btn btn-danger" onclick="uninstallPackage()">🗑️ Uninstall</button>
567
+
568
+ <hr>
569
+ <div class="card-title" style="margin-bottom:14px"><span class="icon">📊</span> Device Info</div>
570
+ <button class="btn btn-secondary btn-sm" onclick="loadDeviceInfo()" style="margin-bottom:12px">Load Info</button>
571
+ <pre id="device-info" style="font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-dim); white-space:pre-wrap; line-height:1.8; max-height:200px; overflow-y:auto"></pre>
572
+ </div>
573
+ </div>
574
+ </div>
575
+
576
+ <!-- ══════════════════ TAB: ACTIONS ══════════════════ -->
577
+ <div class="tab-panel" id="panel-actions">
578
+ <div class="card section-full">
579
+ <div class="card-header">
580
+ <div class="card-title"><span class="icon">⚡</span> Quick ADB Actions</div>
581
+ <span style="font-size:12px; color:var(--text-muted)">Acts on selected device</span>
582
+ </div>
583
+ <div class="quick-actions">
584
+ <button class="action-btn" onclick="doAction('reboot')">
585
+ <span class="a-icon">🔄</span> Reboot
586
+ </button>
587
+ <button class="action-btn" onclick="doAction('reboot-recovery')">
588
+ <span class="a-icon">🛠️</span> Recovery
589
+ </button>
590
+ <button class="action-btn" onclick="doAction('reboot-bootloader')">
591
+ <span class="a-icon">🔓</span> Bootloader
592
+ </button>
593
+ <button class="action-btn" onclick="doAction('reboot-fastboot')">
594
+ <span class="a-icon">⚡</span> Fastboot
595
+ </button>
596
+ <button class="action-btn" onclick="doAction('kill-server')">
597
+ <span class="a-icon">🛑</span> Kill Server
598
+ </button>
599
+ <button class="action-btn" onclick="doAction('start-server')">
600
+ <span class="a-icon">▶️</span> Start Server
601
+ </button>
602
+ <button class="action-btn" onclick="doAction('logcat')">
603
+ <span class="a-icon">📜</span> Get Logcat
604
+ </button>
605
+ <button class="action-btn" onclick="captureScreen()">
606
+ <span class="a-icon">📸</span> Screenshot
607
+ </button>
608
+ </div>
609
+ </div>
610
+
611
+ <!-- Log Console for actions too -->
612
+ <div class="log-console">
613
+ <div class="log-console-header">
614
+ <div class="title"><div class="live-dot"></div> ADB Output</div>
615
+ <button class="btn btn-secondary btn-sm" onclick="clearLog()">Clear</button>
616
+ </div>
617
+ <div id="log-output-2">
618
+ <div class="log-empty" style="padding:16px;">Waiting for actions…</div>
619
+ </div>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- ══════════════════ TAB: SETTINGS ══════════════════ -->
624
+ <div class="tab-panel" id="panel-settings">
625
+ <div class="card" style="max-width:600px">
626
+ <div class="card-title" style="margin-bottom:20px"><span class="icon">⚙️</span> ADB Configuration</div>
627
+
628
+ <div class="form-row">
629
+ <div class="form-label">ADB Executable Path</div>
630
+ <div class="input-group">
631
+ <input type="text" id="adb-path-input" placeholder="adb or C:\platform-tools\adb.exe">
632
+ <button class="btn btn-primary" onclick="saveAdbPath()">Save</button>
633
+ </div>
634
+ <div style="font-size:12px; color:var(--text-muted); margin-top:6px">
635
+ Leave blank or "adb" if ADB is in your system PATH.<br>
636
+ Otherwise enter the full path, e.g. <code style="color:var(--accent2)">C:\platform-tools\adb.exe</code>
637
+ </div>
638
+ </div>
639
+
640
+ <hr>
641
+ <div class="card-title" style="margin-bottom:16px"><span class="icon">📥</span> How to Install ADB</div>
642
+ <div style="font-size:13px; color:var(--text-dim); line-height:1.8">
643
+ <p><strong style="color:var(--text)">Option 1 – Platform Tools (Recommended)</strong></p>
644
+ <p>1. Download <a href="https://developer.android.com/tools/releases/platform-tools" target="_blank" style="color:var(--accent2)">Android Platform Tools</a></p>
645
+ <p>2. Extract to <code style="color:var(--accent2)">C:\platform-tools\</code></p>
646
+ <p>3. Add to PATH or set the path above</p>
647
+ <br>
648
+ <p><strong style="color:var(--text)">Option 2 – Enable Developer Mode on Phone</strong></p>
649
+ <p>1. Settings → About Phone → tap Build Number 7 times</p>
650
+ <p>2. Settings → Developer Options → Enable USB Debugging</p>
651
+ <p>3. Connect phone via USB → Accept debugging prompt</p>
652
+ </div>
653
+
654
+ <hr>
655
+ <div class="card-title" style="margin-bottom:16px"><span class="icon">🔧</span> Test ADB Connection</div>
656
+ <button class="btn btn-secondary" onclick="testAdb()">Run ADB Version Check</button>
657
+ <pre id="adb-test-output" style="margin-top:12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:var(--text-dim); white-space:pre-wrap; line-height:1.8"></pre>
658
+ </div>
659
+ </div>
660
+
661
+ </div>
662
+
663
+ <!-- Toast container -->
664
+ <div id="toast-container"></div>
665
+
666
+ <!-- Confirm Modal -->
667
+ <div id="confirm-overlay">
668
+ <div id="confirm-box">
669
+ <div id="confirm-icon">⚠️</div>
670
+ <div id="confirm-title">Are you sure?</div>
671
+ <div id="confirm-msg">This action cannot be undone.</div>
672
+ <div class="confirm-actions">
673
+ <button id="confirm-cancel">Cancel</button>
674
+ <button id="confirm-ok" class="danger">Confirm</button>
675
+ </div>
676
+ </div>
677
+ </div>
678
+
679
+ <script>
680
+ // ══════════════════ State ══════════════════
681
+ let selectedSerial = null;
682
+ let selectedApkFile = null;
683
+ let allPackages = [];
684
+ let ws = null;
685
+
686
+ // ══════════════════ Custom Confirm Modal ══════════════════
687
+ function showConfirm({ icon = '⚠️', title = 'Are you sure?', msg = '', okText = 'Confirm', okStyle = 'danger' }) {
688
+ return new Promise(resolve => {
689
+ const overlay = document.getElementById('confirm-overlay');
690
+ document.getElementById('confirm-icon').textContent = icon;
691
+ document.getElementById('confirm-title').textContent = title;
692
+ document.getElementById('confirm-msg').textContent = msg;
693
+ const okBtn = document.getElementById('confirm-ok');
694
+ okBtn.textContent = okText;
695
+ okBtn.className = okStyle;
696
+ overlay.classList.add('open');
697
+
698
+ const cleanup = (result) => {
699
+ overlay.classList.remove('open');
700
+ okBtn.removeEventListener('click', onOk);
701
+ document.getElementById('confirm-cancel').removeEventListener('click', onCancel);
702
+ overlay.removeEventListener('click', onOverlay);
703
+ resolve(result);
704
+ };
705
+ const onOk = () => cleanup(true);
706
+ const onCancel = () => cleanup(false);
707
+ const onOverlay = (e) => { if (e.target === overlay) cleanup(false); };
708
+
709
+ okBtn.addEventListener('click', onOk);
710
+ document.getElementById('confirm-cancel').addEventListener('click', onCancel);
711
+ overlay.addEventListener('click', onOverlay);
712
+ });
713
+ }
714
+
715
+ // ══════════════════ WebSocket ══════════════════
716
+ function connectWs() {
717
+ ws = new WebSocket(`ws://${location.host}`);
718
+ ws.onmessage = e => {
719
+ try {
720
+ const d = JSON.parse(e.data);
721
+ if (d.type === 'log') appendLog(d);
722
+ } catch {}
723
+ };
724
+ ws.onclose = () => setTimeout(connectWs, 2000);
725
+ }
726
+ connectWs();
727
+
728
+ // ══════════════════ Log ══════════════════
729
+ let logInitialized = false;
730
+ function appendLog(d) {
731
+ const out = document.getElementById('log-output');
732
+ const out2 = document.getElementById('log-output-2');
733
+ if (!logInitialized) { out.innerHTML = ''; logInitialized = true; }
734
+
735
+ const time = new Date(d.timestamp).toLocaleTimeString();
736
+ const msg = d.message || '';
737
+ const line = `<div class="log-line"><span class="log-time">${time}</span><span class="log-msg ${d.level}">${escapeHtml(msg)}</span></div>`;
738
+ [out, out2].forEach(el => {
739
+ if (!el) return;
740
+ if (el.querySelector('.log-empty')) el.innerHTML = '';
741
+ el.insertAdjacentHTML('beforeend', line);
742
+ el.scrollTop = el.scrollHeight;
743
+ });
744
+ }
745
+
746
+ function clearLog() {
747
+ ['log-output','log-output-2'].forEach(id => {
748
+ const el = document.getElementById(id);
749
+ el.innerHTML = '<div class="log-empty">Log cleared.</div>';
750
+ });
751
+ logInitialized = false;
752
+ }
753
+
754
+ function escapeHtml(s) {
755
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
756
+ }
757
+
758
+ // ══════════════════ Toast ══════════════════
759
+ function toast(msg, type = 'info') {
760
+ const icons = { success: '✅', error: '❌', info: 'ℹ️', warn: '⚠️' };
761
+ const el = document.createElement('div');
762
+ el.className = `toast ${type}`;
763
+ el.innerHTML = `<span>${icons[type] || '•'}</span><span>${escapeHtml(msg)}</span>`;
764
+ document.getElementById('toast-container').appendChild(el);
765
+ setTimeout(() => {
766
+ el.classList.add('out');
767
+ setTimeout(() => el.remove(), 300);
768
+ }, 3500);
769
+ }
770
+
771
+ // ══════════════════ Tabs ══════════════════
772
+ function switchTab(name) {
773
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
774
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
775
+ document.getElementById('tab-' + name).classList.add('active');
776
+ document.getElementById('panel-' + name).classList.add('active');
777
+ }
778
+
779
+ // ══════════════════ ADB Status ══════════════════
780
+ async function checkAdbStatus() {
781
+ try {
782
+ const r = await fetch('/api/adb/status').then(r => r.json());
783
+ const dot = document.getElementById('adb-dot');
784
+ const txt = document.getElementById('adb-status-text');
785
+ if (r.ok) {
786
+ dot.className = 'status-dot connected';
787
+ const ver = r.version.split('\n')[0].replace('Android Debug Bridge version ','ADB ');
788
+ txt.textContent = ver;
789
+ } else {
790
+ dot.className = 'status-dot error';
791
+ txt.textContent = 'ADB not found';
792
+ }
793
+ } catch {
794
+ document.getElementById('adb-dot').className = 'status-dot error';
795
+ document.getElementById('adb-status-text').textContent = 'Server error';
796
+ }
797
+ }
798
+
799
+ // ══════════════════ Devices ══════════════════
800
+ async function loadDevices() {
801
+ const btn = document.getElementById('btn-refresh-devices');
802
+ btn.disabled = true; btn.textContent = '…';
803
+ try {
804
+ const { devices, ok, error } = await fetch('/api/devices').then(r => r.json());
805
+ const el = document.getElementById('device-list');
806
+ if (!ok || !devices.length) {
807
+ el.innerHTML = `<div class="no-devices">
808
+ <div class="big">📡</div>
809
+ <div>${error || 'No devices connected'}</div>
810
+ <div style="margin-top:6px;font-size:12px">Connect phone via USB with USB Debugging enabled</div>
811
+ </div>`;
812
+ return;
813
+ }
814
+ el.innerHTML = devices.map(d => `
815
+ <div class="device-item ${selectedSerial === d.serial ? 'selected' : ''}"
816
+ onclick="selectDevice('${d.serial}','${d.model}')" id="dev-${d.serial}">
817
+ <span class="device-icon">📱</span>
818
+ <div class="device-info">
819
+ <div class="device-name">${d.model.replace(/_/g,' ')}</div>
820
+ <div class="device-serial">${d.serial}${d.product ? ' · ' + d.product : ''}</div>
821
+ </div>
822
+ <span class="device-state ${d.state}">${d.state}</span>
823
+ </div>
824
+ `).join('');
825
+ } finally {
826
+ btn.disabled = false; btn.textContent = 'Refresh';
827
+ }
828
+ }
829
+
830
+ function selectDevice(serial, model) {
831
+ selectedSerial = serial;
832
+ document.querySelectorAll('.device-item').forEach(el => el.classList.remove('selected'));
833
+ const el = document.getElementById('dev-' + serial);
834
+ if (el) el.classList.add('selected');
835
+ document.getElementById('selected-serial-display').textContent = serial + ' (' + model + ')';
836
+ toast('Selected: ' + model, 'info');
837
+ }
838
+
839
+ // ══════════════════ APK Upload ══════════════════
840
+ const dropZone = document.getElementById('drop-zone');
841
+ const fileInput = document.getElementById('file-input');
842
+
843
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragging'); });
844
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragging'));
845
+ dropZone.addEventListener('drop', e => {
846
+ e.preventDefault(); dropZone.classList.remove('dragging');
847
+ const file = e.dataTransfer.files[0];
848
+ if (file) uploadFile(file);
849
+ });
850
+ fileInput.addEventListener('change', e => {
851
+ if (e.target.files[0]) uploadFile(e.target.files[0]);
852
+ });
853
+
854
+ function uploadFile(file) {
855
+ if (!file.name.endsWith('.apk')) { toast('Only .apk files are supported', 'error'); return; }
856
+ const progress = document.getElementById('upload-progress');
857
+ const bar = document.getElementById('upload-bar');
858
+ progress.style.display = 'block'; bar.style.width = '0%';
859
+
860
+ const fd = new FormData();
861
+ fd.append('apk', file);
862
+ const xhr = new XMLHttpRequest();
863
+ xhr.open('POST', '/api/upload');
864
+ xhr.upload.onprogress = e => {
865
+ if (e.lengthComputable) bar.style.width = (e.loaded / e.total * 100) + '%';
866
+ };
867
+ xhr.onload = () => {
868
+ progress.style.display = 'none'; bar.style.width = '0%'; fileInput.value = '';
869
+ try {
870
+ const r = JSON.parse(xhr.responseText);
871
+ if (r.ok) { toast('Uploaded: ' + r.originalName, 'success'); loadApks(); }
872
+ else toast('Upload failed: ' + r.error, 'error');
873
+ } catch { toast('Upload error', 'error'); }
874
+ };
875
+ xhr.onerror = () => { progress.style.display = 'none'; toast('Network error', 'error'); };
876
+ xhr.send(fd);
877
+ }
878
+
879
+ // ══════════════════ APK List ══════════════════
880
+ async function loadApks() {
881
+ const { files } = await fetch('/api/apks').then(r => r.json());
882
+ const el = document.getElementById('apk-list');
883
+ const badge = document.getElementById('apk-count-badge');
884
+ badge.textContent = (files?.length || 0) + ' APKs';
885
+ if (!files?.length) { el.innerHTML = '<div class="log-empty">No APKs uploaded yet.</div>'; return; }
886
+
887
+ el.innerHTML = files.map(f => {
888
+ const name = f.filename.replace(/^\d+-\d+-/, '');
889
+ const size = formatBytes(f.size);
890
+ const isSelected = selectedApkFile === f.filename;
891
+ return `<div class="apk-item ${isSelected ? 'selected' : ''}" id="apk-${f.filename.replace(/[^a-z0-9]/gi,'_')}">
892
+ <span class="apk-icon">📦</span>
893
+ <div class="apk-info">
894
+ <div class="apk-name" title="${name}">${name}</div>
895
+ <div class="apk-size">${size}</div>
896
+ </div>
897
+ <div class="apk-actions">
898
+ <button class="btn btn-green btn-sm" onclick="selectApk('${f.filename}','${name}')">Select</button>
899
+ <button class="btn btn-danger btn-sm" onclick="deleteApk('${f.filename}')">✕</button>
900
+ </div>
901
+ </div>`;
902
+ }).join('');
903
+ }
904
+
905
+ function selectApk(filename, displayName) {
906
+ selectedApkFile = filename;
907
+ document.querySelectorAll('.apk-item').forEach(el => el.classList.remove('selected'));
908
+ const key = filename.replace(/[^a-z0-9]/gi,'_');
909
+ const el = document.getElementById('apk-' + key);
910
+ if (el) el.classList.add('selected');
911
+ document.getElementById('selected-apk-name').textContent = displayName;
912
+ document.getElementById('install-panel').style.display = 'block';
913
+ }
914
+
915
+ function clearInstallSelection() {
916
+ selectedApkFile = null;
917
+ document.querySelectorAll('.apk-item').forEach(el => el.classList.remove('selected'));
918
+ document.getElementById('install-panel').style.display = 'none';
919
+ }
920
+
921
+ async function deleteApk(filename) {
922
+ const name = filename.replace(/^\d+-\d+-/, '');
923
+ const ok = await showConfirm({ icon: '🗑️', title: 'Delete APK?', msg: `"${name}" will be permanently removed from the library.`, okText: 'Delete', okStyle: 'danger' });
924
+ if (!ok) return;
925
+ const r = await fetch('/api/apks/' + encodeURIComponent(filename), { method: 'DELETE' }).then(r => r.json());
926
+ if (r.ok) { toast('Deleted', 'success'); if (selectedApkFile === filename) clearInstallSelection(); loadApks(); }
927
+ else toast('Delete failed: ' + r.error, 'error');
928
+ }
929
+
930
+ // ══════════════════ Install ══════════════════
931
+ async function installSelected() {
932
+ if (!selectedApkFile) { toast('Select an APK first', 'warn'); return; }
933
+
934
+ const apkName = document.getElementById('selected-apk-name').textContent;
935
+ const device = selectedSerial ? selectedSerial : 'any connected device';
936
+ const ok = await showConfirm({
937
+ icon: '🚀',
938
+ title: 'Install APK?',
939
+ msg: `Install "${apkName}" on ${device}?`,
940
+ okText: 'Install',
941
+ okStyle: 'primary'
942
+ });
943
+ if (!ok) return;
944
+
945
+ const btn = document.getElementById('btn-install');
946
+ btn.disabled = true;
947
+ btn.innerHTML = '<span class="spinner"></span> Installing…';
948
+
949
+ const flags = {
950
+ replace: document.getElementById('opt-replace').checked,
951
+ grant: document.getElementById('opt-grant').checked,
952
+ downgrade: document.getElementById('opt-downgrade').checked,
953
+ test: document.getElementById('opt-test').checked,
954
+ };
955
+
956
+ try {
957
+ const r = await fetch('/api/install', {
958
+ method: 'POST',
959
+ headers: { 'Content-Type': 'application/json' },
960
+ body: JSON.stringify({ filename: selectedApkFile, serial: selectedSerial, flags })
961
+ }).then(r => r.json());
962
+
963
+ if (r.ok) toast('Installation started — check the log', 'success');
964
+ else toast('Error: ' + r.error, 'error');
965
+ } finally {
966
+ btn.disabled = false;
967
+ btn.innerHTML = '<span>🚀</span> Install APK';
968
+ }
969
+ }
970
+
971
+ // ══════════════════ Quick Actions ══════════════════
972
+ const ACTION_META = {
973
+ 'reboot': { icon: '🔄', title: 'Reboot Device?', msg: 'The device will restart normally.', okText: 'Reboot', okStyle: 'danger' },
974
+ 'reboot-recovery': { icon: '🛠️', title: 'Reboot to Recovery?', msg: 'The device will restart into Recovery mode.', okText: 'Recovery', okStyle: 'danger' },
975
+ 'reboot-bootloader': { icon: '🔓', title: 'Reboot to Bootloader?', msg: 'The device will restart into Bootloader/Fastboot.', okText: 'Bootloader', okStyle: 'danger' },
976
+ 'reboot-fastboot': { icon: '⚡', title: 'Reboot to Fastboot?', msg: 'The device will restart into Fastboot mode.', okText: 'Fastboot', okStyle: 'danger' },
977
+ 'kill-server': { icon: '🛑', title: 'Kill ADB Server?', msg: 'The ADB server will be stopped. Reconnect needed.', okText: 'Kill Server', okStyle: 'danger' },
978
+ 'start-server': { icon: '▶️', title: 'Start ADB Server?', msg: 'The ADB server will be started.', okText: 'Start', okStyle: 'primary' },
979
+ 'logcat': { icon: '📜', title: 'Fetch Logcat?', msg: 'Last 200 logcat lines will be pulled from device.', okText: 'Fetch', okStyle: 'primary' },
980
+ };
981
+
982
+ async function doAction(action) {
983
+ const meta = ACTION_META[action] || { icon: '⚡', title: `Run: ${action}?`, msg: 'This ADB action will be executed.', okText: 'Run', okStyle: 'primary' };
984
+ const confirmed = await showConfirm(meta);
985
+ if (!confirmed) return;
986
+
987
+ switchTab('actions');
988
+ const r = await fetch('/api/adb', {
989
+ method: 'POST',
990
+ headers: { 'Content-Type': 'application/json' },
991
+ body: JSON.stringify({ serial: selectedSerial, action })
992
+ }).then(r => r.json());
993
+ toast(r.ok ? 'Done: ' + action : 'Error: ' + (r.output || action), r.ok ? 'success' : 'error');
994
+ }
995
+
996
+ async function captureScreen() {
997
+ toast('Screenshot via adb exec-out screencap — see log', 'info');
998
+ const r = await fetch('/api/adb', {
999
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1000
+ body: JSON.stringify({ serial: selectedSerial, action: 'logcat' })
1001
+ }).then(r => r.json());
1002
+ }
1003
+
1004
+ // ══════════════════ Packages ══════════════════
1005
+ async function loadPackages() {
1006
+ const el = document.getElementById('packages-list');
1007
+ el.innerHTML = '<div class="log-empty" style="padding:16px">Loading…</div>';
1008
+ try {
1009
+ const r = await fetch('/api/shell', {
1010
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1011
+ body: JSON.stringify({ serial: selectedSerial, command: 'pm list packages' })
1012
+ }).then(r => r.json());
1013
+ if (!r.ok) { el.innerHTML = '<div class="log-empty" style="padding:16px;color:var(--red)">Error: ' + escapeHtml(r.error || r.output) + '</div>'; return; }
1014
+ allPackages = r.output.split('\n').map(l => l.replace('package:','').trim()).filter(Boolean).sort();
1015
+ renderPackages(allPackages);
1016
+ } catch(e) {
1017
+ el.innerHTML = '<div class="log-empty" style="padding:16px;color:var(--red)">Request failed</div>';
1018
+ }
1019
+ }
1020
+
1021
+ function renderPackages(pkgs) {
1022
+ const el = document.getElementById('packages-list');
1023
+ if (!pkgs.length) { el.innerHTML = '<div class="log-empty" style="padding:16px">No packages found.</div>'; return; }
1024
+ el.innerHTML = pkgs.map(p => `
1025
+ <div class="package-item">
1026
+ <span>${escapeHtml(p)}</span>
1027
+ <button class="btn btn-danger btn-sm" onclick="quickUninstall('${escapeHtml(p)}')">Remove</button>
1028
+ </div>
1029
+ `).join('');
1030
+ }
1031
+
1032
+ function filterPackages() {
1033
+ const q = document.getElementById('pkg-search').value.toLowerCase();
1034
+ renderPackages(allPackages.filter(p => p.toLowerCase().includes(q)));
1035
+ }
1036
+
1037
+ function quickUninstall(pkg) {
1038
+ document.getElementById('uninstall-pkg').value = pkg;
1039
+ }
1040
+
1041
+ async function uninstallPackage() {
1042
+ const pkg = document.getElementById('uninstall-pkg').value.trim();
1043
+ if (!pkg) { toast('Enter a package name', 'warn'); return; }
1044
+ const keepData = document.getElementById('uninstall-keep-data').checked;
1045
+ const ok = await showConfirm({
1046
+ icon: '🗑️',
1047
+ title: 'Uninstall Package?',
1048
+ msg: `"${pkg}" will be uninstalled from the device.${keepData ? ' App data will be kept.' : ' All app data will be deleted.'}`,
1049
+ okText: 'Uninstall',
1050
+ okStyle: 'danger'
1051
+ });
1052
+ if (!ok) return;
1053
+ const r = await fetch('/api/uninstall', {
1054
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1055
+ body: JSON.stringify({ serial: selectedSerial, packageName: pkg, keepData })
1056
+ }).then(r => r.json());
1057
+ if (r.ok) toast('Uninstall started — check log', 'success');
1058
+ else toast('Error: ' + r.error, 'error');
1059
+ }
1060
+
1061
+ async function loadDeviceInfo() {
1062
+ const out = document.getElementById('device-info');
1063
+ out.textContent = 'Loading…';
1064
+ try {
1065
+ const commands = [
1066
+ { cmd: 'getprop ro.product.model', label: 'Model' },
1067
+ { cmd: 'getprop ro.build.version.release', label: 'Android' },
1068
+ { cmd: 'getprop ro.build.version.sdk', label: 'SDK' },
1069
+ { cmd: 'getprop ro.product.manufacturer', label: 'Brand' },
1070
+ { cmd: 'wm size', label: 'Screen Size' },
1071
+ { cmd: 'dumpsys battery', label: 'Battery' },
1072
+ ];
1073
+ const results = await Promise.all(commands.map(async c => {
1074
+ try {
1075
+ const r = await fetch('/api/shell', {
1076
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1077
+ body: JSON.stringify({ serial: selectedSerial, command: c.cmd })
1078
+ }).then(r => r.json());
1079
+ return `${c.label}: ${r.output?.trim() || '—'}`;
1080
+ } catch { return `${c.label}: error`; }
1081
+ }));
1082
+ out.textContent = results.join('\n');
1083
+ } catch { out.textContent = 'Error fetching info'; }
1084
+ }
1085
+
1086
+ // ══════════════════ Settings ══════════════════
1087
+ async function loadConfig() {
1088
+ const r = await fetch('/api/config').then(r => r.json());
1089
+ document.getElementById('adb-path-input').value = r.adbPath || '';
1090
+ }
1091
+
1092
+ async function saveAdbPath() {
1093
+ const adbPath = document.getElementById('adb-path-input').value.trim() || 'adb';
1094
+ const r = await fetch('/api/config', {
1095
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1096
+ body: JSON.stringify({ adbPath })
1097
+ }).then(r => r.json());
1098
+ if (r.ok) { toast('ADB path saved!', 'success'); checkAdbStatus(); }
1099
+ else toast('Save failed', 'error');
1100
+ }
1101
+
1102
+ async function testAdb() {
1103
+ const out = document.getElementById('adb-test-output');
1104
+ out.textContent = 'Testing…';
1105
+ const r = await fetch('/api/adb/status').then(r => r.json());
1106
+ out.textContent = r.ok ? r.version : 'Error: ' + r.error;
1107
+ if (r.ok) checkAdbStatus();
1108
+ }
1109
+
1110
+ // ══════════════════ Utils ══════════════════
1111
+ function formatBytes(b) {
1112
+ if (b < 1024) return b + ' B';
1113
+ if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
1114
+ return (b/1048576).toFixed(2) + ' MB';
1115
+ }
1116
+
1117
+ function refreshAll() {
1118
+ checkAdbStatus();
1119
+ loadDevices();
1120
+ loadApks();
1121
+ }
1122
+
1123
+ // ══════════════════ Init ══════════════════
1124
+ checkAdbStatus();
1125
+ loadDevices();
1126
+ loadApks();
1127
+ loadConfig();
1128
+ // Auto-refresh devices every 8s
1129
+ setInterval(loadDevices, 8000);
1130
+ </script>
1131
+ </body>
1132
+ </html>