Update app.py
Browse files
app.py
CHANGED
|
@@ -317,57 +317,15 @@ USER_TEMPLATE = """
|
|
| 317 |
background-color: var(--card-bg);
|
| 318 |
padding: 12px var(--padding);
|
| 319 |
border-radius: var(--border-radius);
|
| 320 |
-
display: flex;
|
| 321 |
-
align-items: center;
|
| 322 |
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 323 |
transition: background-color 0.2s ease;
|
| 324 |
word-break: break-word;
|
| 325 |
}
|
| 326 |
.file-item:hover { background-color: rgba(255, 255, 255, 0.08); }
|
| 327 |
-
|
| 328 |
-
.file-preview-area {
|
| 329 |
-
flex-shrink: 0;
|
| 330 |
-
width: 80px;
|
| 331 |
-
height: 80px;
|
| 332 |
-
margin-right: var(--padding);
|
| 333 |
-
display: flex;
|
| 334 |
-
align-items: center;
|
| 335 |
-
justify-content: center;
|
| 336 |
-
background-color: rgba(0,0,0,0.05);
|
| 337 |
-
border-radius: 8px;
|
| 338 |
-
overflow: hidden;
|
| 339 |
-
}
|
| 340 |
-
.file-preview-image,
|
| 341 |
-
.file-preview-video {
|
| 342 |
-
max-width: 100%;
|
| 343 |
-
max-height: 100%;
|
| 344 |
-
object-fit: cover;
|
| 345 |
-
display: block;
|
| 346 |
-
}
|
| 347 |
-
.file-preview-video {
|
| 348 |
-
width: 100%;
|
| 349 |
-
height: 100%;
|
| 350 |
-
}
|
| 351 |
-
.file-preview-audio {
|
| 352 |
-
width: 100%;
|
| 353 |
-
transform: scale(0.85);
|
| 354 |
-
transform-origin: center;
|
| 355 |
-
}
|
| 356 |
-
.file-preview-icon {
|
| 357 |
-
font-size: 0.8em;
|
| 358 |
-
font-weight: bold;
|
| 359 |
-
color: var(--tg-theme-hint-color);
|
| 360 |
-
padding: 5px;
|
| 361 |
-
text-align: center;
|
| 362 |
-
text-transform: uppercase;
|
| 363 |
-
word-break: break-all;
|
| 364 |
-
line-height: 1.2;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
.file-info { flex-grow: 1; margin-right: 10px; overflow: hidden; min-width: 0;}
|
| 368 |
.file-name { font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 369 |
.file-meta { font-size: 0.8em; color: var(--tg-theme-hint-color); }
|
| 370 |
-
.file-actions { flex-shrink: 0; }
|
| 371 |
.file-actions a, .file-actions button {
|
| 372 |
display: inline-block;
|
| 373 |
padding: 6px 10px;
|
|
@@ -386,6 +344,33 @@ USER_TEMPLATE = """
|
|
| 386 |
.loading, .no-files { text-align: center; padding: 20px; color: var(--tg-theme-hint-color); }
|
| 387 |
.progress-bar { width: 100%; background-color: #ddd; border-radius: 4px; height: 8px; margin-top: 5px; display: none; }
|
| 388 |
.progress-bar-inner { height: 100%; width: 0%; background-color: var(--tg-theme-button-color); border-radius: 4px; transition: width 0.1s linear; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
.spinner {
|
| 390 |
border: 4px solid rgba(255, 255, 255, 0.3);
|
| 391 |
border-radius: 50%;
|
|
@@ -422,6 +407,12 @@ USER_TEMPLATE = """
|
|
| 422 |
</ul>
|
| 423 |
</section>
|
| 424 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
<script>
|
| 426 |
const tg = window.Telegram.WebApp;
|
| 427 |
const MAX_FILES = {{ max_files }};
|
|
@@ -437,6 +428,10 @@ USER_TEMPLATE = """
|
|
| 437 |
const loadingSpinner = document.getElementById('loading-spinner');
|
| 438 |
const progressBar = document.getElementById('progress-bar');
|
| 439 |
const progressBarInner = document.getElementById('progress-bar-inner');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
function applyTheme(themeParams) {
|
| 442 |
const root = document.documentElement;
|
|
@@ -475,45 +470,9 @@ USER_TEMPLATE = """
|
|
| 475 |
}
|
| 476 |
noFilesMessage.style.display = 'none';
|
| 477 |
files.sort((a, b) => b.uploaded_at_ts - a.uploaded_at_ts);
|
| 478 |
-
|
| 479 |
files.forEach(file => {
|
| 480 |
const li = document.createElement('li');
|
| 481 |
li.classList.add('file-item');
|
| 482 |
-
|
| 483 |
-
const previewDiv = document.createElement('div');
|
| 484 |
-
previewDiv.classList.add('file-preview-area');
|
| 485 |
-
const downloadUrl = `/download/${encodeURIComponent(file.filename)}?initData=${encodeURIComponent(userInitData)}`;
|
| 486 |
-
const mimeType = file.content_type || '';
|
| 487 |
-
|
| 488 |
-
if (mimeType.startsWith('image/')) {
|
| 489 |
-
const img = document.createElement('img');
|
| 490 |
-
img.src = downloadUrl;
|
| 491 |
-
img.alt = file.filename;
|
| 492 |
-
img.classList.add('file-preview-image');
|
| 493 |
-
previewDiv.appendChild(img);
|
| 494 |
-
} else if (mimeType.startsWith('video/')) {
|
| 495 |
-
const video = document.createElement('video');
|
| 496 |
-
video.src = downloadUrl;
|
| 497 |
-
video.controls = true;
|
| 498 |
-
video.classList.add('file-preview-video');
|
| 499 |
-
video.preload = "metadata";
|
| 500 |
-
previewDiv.appendChild(video);
|
| 501 |
-
} else if (mimeType.startsWith('audio/')) {
|
| 502 |
-
const audio = document.createElement('audio');
|
| 503 |
-
audio.src = downloadUrl;
|
| 504 |
-
audio.controls = true;
|
| 505 |
-
audio.classList.add('file-preview-audio');
|
| 506 |
-
audio.preload = "metadata";
|
| 507 |
-
previewDiv.appendChild(audio);
|
| 508 |
-
} else {
|
| 509 |
-
const iconPlaceholder = document.createElement('div');
|
| 510 |
-
iconPlaceholder.classList.add('file-preview-icon');
|
| 511 |
-
const ext = file.filename.split('.').pop().toLowerCase() || 'file';
|
| 512 |
-
iconPlaceholder.textContent = ext.substring(0,4);
|
| 513 |
-
previewDiv.appendChild(iconPlaceholder);
|
| 514 |
-
}
|
| 515 |
-
li.appendChild(previewDiv);
|
| 516 |
-
|
| 517 |
const fileInfoDiv = document.createElement('div');
|
| 518 |
fileInfoDiv.classList.add('file-info');
|
| 519 |
const fileNameSpan = document.createElement('span');
|
|
@@ -526,18 +485,31 @@ USER_TEMPLATE = """
|
|
| 526 |
const size = file.size ? formatBytes(file.size) : '';
|
| 527 |
fileMetaSpan.textContent = `${date}${size ? ' - ' + size : ''}`;
|
| 528 |
fileInfoDiv.appendChild(fileMetaSpan);
|
| 529 |
-
li.appendChild(fileInfoDiv);
|
| 530 |
-
|
| 531 |
const fileActionsDiv = document.createElement('div');
|
| 532 |
fileActionsDiv.classList.add('file-actions');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
const downloadLink = document.createElement('a');
|
| 534 |
downloadLink.classList.add('download-btn');
|
| 535 |
downloadLink.textContent = 'Скачать';
|
| 536 |
-
downloadLink.href =
|
| 537 |
downloadLink.setAttribute('download', file.filename);
|
| 538 |
downloadLink.title = 'Скачать файл';
|
| 539 |
downloadLink.onclick = (e) => e.stopPropagation();
|
| 540 |
fileActionsDiv.appendChild(downloadLink);
|
|
|
|
| 541 |
li.appendChild(fileActionsDiv);
|
| 542 |
fileListUl.appendChild(li);
|
| 543 |
});
|
|
@@ -648,19 +620,67 @@ USER_TEMPLATE = """
|
|
| 648 |
uploadStatusDiv.style.color = 'red';
|
| 649 |
} finally {
|
| 650 |
progressBar.style.display = 'none';
|
| 651 |
-
if (currentFiles.length > 0
|
| 652 |
uploadButton.classList.add('enabled');
|
| 653 |
uploadButton.disabled = false;
|
| 654 |
-
} else {
|
| 655 |
uploadButton.classList.remove('enabled');
|
| 656 |
uploadButton.disabled = true;
|
| 657 |
-
selectedFilesDiv.textContent = 'Файлы не выбраны';
|
| 658 |
-
fileInput.value = ''; // Clear file input on final step
|
| 659 |
-
currentFiles = [];
|
| 660 |
}
|
| 661 |
}
|
| 662 |
}
|
| 663 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
function setupTelegram() {
|
| 665 |
if (!tg || !tg.initData) {
|
| 666 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
|
@@ -866,12 +886,46 @@ ADMIN_USER_FILES_TEMPLATE = """
|
|
| 866 |
.file-table td { font-size: 0.95em; word-break: break-word; }
|
| 867 |
.filename { font-weight: 500; }
|
| 868 |
.filesize, .filedate { color: var(--admin-secondary); font-size: 0.9em; }
|
| 869 |
-
.actions a {
|
| 870 |
-
display: inline-block; padding: 6px
|
| 871 |
-
text-decoration: none;
|
|
|
|
|
|
|
| 872 |
}
|
| 873 |
-
.actions a:hover {
|
|
|
|
|
|
|
|
|
|
| 874 |
.no-files { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
@media screen and (max-width: 768px) {
|
| 876 |
.file-table { border: 0; box-shadow: none; }
|
| 877 |
.file-table thead { display: none; }
|
|
@@ -892,6 +946,7 @@ ADMIN_USER_FILES_TEMPLATE = """
|
|
| 892 |
<a href="{{ url_for('admin_panel') }}" class="back-link">← Назад к списку пользователей</a>
|
| 893 |
<h1>Файлы пользователя</h1>
|
| 894 |
<div class="user-identifier">{{ user_info.first_name or '' }} {{ user_info.last_name or '' }} (ID: {{ user_id }})</div>
|
|
|
|
| 895 |
{% if files %}
|
| 896 |
<table class="file-table">
|
| 897 |
<thead>
|
|
@@ -904,14 +959,19 @@ ADMIN_USER_FILES_TEMPLATE = """
|
|
| 904 |
</tr>
|
| 905 |
</thead>
|
| 906 |
<tbody>
|
| 907 |
-
{% for
|
| 908 |
<tr>
|
| 909 |
-
<td data-label="Имя файла" class="filename">{{
|
| 910 |
-
<td data-label="Размер" class="filesize">{{
|
| 911 |
-
<td data-label="Дата" class="filedate">{{
|
| 912 |
-
<td data-label="Тип">{{
|
| 913 |
<td data-label="Действия" class="actions">
|
| 914 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 915 |
</td>
|
| 916 |
</tr>
|
| 917 |
{% endfor %}
|
|
@@ -921,6 +981,14 @@ ADMIN_USER_FILES_TEMPLATE = """
|
|
| 921 |
<p class="no-files">У этого пользователя нет загруженных файлов.</p>
|
| 922 |
{% endif %}
|
| 923 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 924 |
<script>
|
| 925 |
function formatBytes(bytes, decimals = 2) {
|
| 926 |
if (!+bytes) return '0 Bytes'
|
|
@@ -930,6 +998,62 @@ ADMIN_USER_FILES_TEMPLATE = """
|
|
| 930 |
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
| 931 |
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
| 932 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
</script>
|
| 934 |
</body>
|
| 935 |
</html>
|
|
@@ -1100,10 +1224,9 @@ def admin_user_files(user_id):
|
|
| 1100 |
user_info=user_info,
|
| 1101 |
files=files)
|
| 1102 |
|
| 1103 |
-
|
| 1104 |
-
def admin_download_file(user_id, filename):
|
| 1105 |
user_id_str = str(user_id)
|
| 1106 |
-
logging.info(f"Admin
|
| 1107 |
api = get_hf_api(write=False)
|
| 1108 |
if not api:
|
| 1109 |
return "Server error: Cannot connect to storage.", 500
|
|
@@ -1123,22 +1246,30 @@ def admin_download_file(user_id, filename):
|
|
| 1123 |
force_download=False,
|
| 1124 |
etag_timeout=10
|
| 1125 |
)
|
| 1126 |
-
logging.info(f"Admin download: File {path_in_repo} cached at {local_file_path}")
|
| 1127 |
content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
| 1128 |
if file_metadata and 'content_type' in file_metadata:
|
| 1129 |
content_type = file_metadata['content_type'] or content_type
|
| 1130 |
return send_file(
|
| 1131 |
local_file_path,
|
| 1132 |
mimetype=content_type,
|
| 1133 |
-
as_attachment=
|
| 1134 |
-
download_name=filename
|
| 1135 |
)
|
| 1136 |
except EntryNotFoundError:
|
| 1137 |
-
logging.error(f"Admin
|
| 1138 |
return "File not found on storage.", 404
|
| 1139 |
except Exception as e:
|
| 1140 |
-
logging.error(f"Admin
|
| 1141 |
-
return "Server error during
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1142 |
|
| 1143 |
@app.route('/admin/download_metadata', methods=['POST'])
|
| 1144 |
def admin_trigger_download_metadata():
|
|
|
|
| 317 |
background-color: var(--card-bg);
|
| 318 |
padding: 12px var(--padding);
|
| 319 |
border-radius: var(--border-radius);
|
| 320 |
+
display: flex; align-items: center; justify-content: space-between;
|
|
|
|
| 321 |
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 322 |
transition: background-color 0.2s ease;
|
| 323 |
word-break: break-word;
|
| 324 |
}
|
| 325 |
.file-item:hover { background-color: rgba(255, 255, 255, 0.08); }
|
| 326 |
+
.file-info { flex-grow: 1; margin-right: 10px; overflow: hidden; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
.file-name { font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 328 |
.file-meta { font-size: 0.8em; color: var(--tg-theme-hint-color); }
|
|
|
|
| 329 |
.file-actions a, .file-actions button {
|
| 330 |
display: inline-block;
|
| 331 |
padding: 6px 10px;
|
|
|
|
| 344 |
.loading, .no-files { text-align: center; padding: 20px; color: var(--tg-theme-hint-color); }
|
| 345 |
.progress-bar { width: 100%; background-color: #ddd; border-radius: 4px; height: 8px; margin-top: 5px; display: none; }
|
| 346 |
.progress-bar-inner { height: 100%; width: 0%; background-color: var(--tg-theme-button-color); border-radius: 4px; transition: width 0.1s linear; }
|
| 347 |
+
.modal {
|
| 348 |
+
display: none; position: fixed; z-index: 1001;
|
| 349 |
+
left: 0; top: 0; width: 100%; height: 100%;
|
| 350 |
+
overflow: auto; background-color: rgba(0,0,0,0.8);
|
| 351 |
+
backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
|
| 352 |
+
animation: fadeIn 0.3s ease-out;
|
| 353 |
+
}
|
| 354 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 355 |
+
.modal-content {
|
| 356 |
+
margin: auto; display: block; max-width: 90%; max-height: 85%;
|
| 357 |
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 358 |
+
}
|
| 359 |
+
.modal-content img, .modal-content video, .modal-content audio {
|
| 360 |
+
display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; margin: auto;
|
| 361 |
+
background-color: var(--tg-theme-bg-color);
|
| 362 |
+
}
|
| 363 |
+
.modal-close {
|
| 364 |
+
position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px;
|
| 365 |
+
font-weight: bold; transition: 0.3s; cursor: pointer; z-index: 1002;
|
| 366 |
+
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
| 367 |
+
}
|
| 368 |
+
.modal-close:hover, .modal-close:focus { color: #bbb; text-decoration: none; }
|
| 369 |
+
.modal-caption {
|
| 370 |
+
margin: auto; display: block; width: 80%; max-width: 700px; text-align: center;
|
| 371 |
+
color: #ccc; padding: 10px 0; height: 50px; position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%);
|
| 372 |
+
background: rgba(0,0,0,0.5); border-radius: 8px;
|
| 373 |
+
}
|
| 374 |
.spinner {
|
| 375 |
border: 4px solid rgba(255, 255, 255, 0.3);
|
| 376 |
border-radius: 50%;
|
|
|
|
| 407 |
</ul>
|
| 408 |
</section>
|
| 409 |
</div>
|
| 410 |
+
<div id="viewerModal" class="modal">
|
| 411 |
+
<span class="modal-close" id="modalCloseBtn">×</span>
|
| 412 |
+
<div id="modalContent" class="modal-content">
|
| 413 |
+
</div>
|
| 414 |
+
<div id="modalCaption" class="modal-caption"></div>
|
| 415 |
+
</div>
|
| 416 |
<script>
|
| 417 |
const tg = window.Telegram.WebApp;
|
| 418 |
const MAX_FILES = {{ max_files }};
|
|
|
|
| 428 |
const loadingSpinner = document.getElementById('loading-spinner');
|
| 429 |
const progressBar = document.getElementById('progress-bar');
|
| 430 |
const progressBarInner = document.getElementById('progress-bar-inner');
|
| 431 |
+
const modal = document.getElementById('viewerModal');
|
| 432 |
+
const modalContent = document.getElementById('modalContent');
|
| 433 |
+
const modalCaption = document.getElementById('modalCaption');
|
| 434 |
+
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
| 435 |
|
| 436 |
function applyTheme(themeParams) {
|
| 437 |
const root = document.documentElement;
|
|
|
|
| 470 |
}
|
| 471 |
noFilesMessage.style.display = 'none';
|
| 472 |
files.sort((a, b) => b.uploaded_at_ts - a.uploaded_at_ts);
|
|
|
|
| 473 |
files.forEach(file => {
|
| 474 |
const li = document.createElement('li');
|
| 475 |
li.classList.add('file-item');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
const fileInfoDiv = document.createElement('div');
|
| 477 |
fileInfoDiv.classList.add('file-info');
|
| 478 |
const fileNameSpan = document.createElement('span');
|
|
|
|
| 485 |
const size = file.size ? formatBytes(file.size) : '';
|
| 486 |
fileMetaSpan.textContent = `${date}${size ? ' - ' + size : ''}`;
|
| 487 |
fileInfoDiv.appendChild(fileMetaSpan);
|
|
|
|
|
|
|
| 488 |
const fileActionsDiv = document.createElement('div');
|
| 489 |
fileActionsDiv.classList.add('file-actions');
|
| 490 |
+
const mimeType = file.content_type || '';
|
| 491 |
+
if (mimeType.startsWith('image/') || mimeType.startsWith('video/') || mimeType.startsWith('audio/')) {
|
| 492 |
+
const viewButton = document.createElement('button');
|
| 493 |
+
viewButton.textContent = '👁️';
|
| 494 |
+
viewButton.classList.add('view-btn');
|
| 495 |
+
viewButton.style.backgroundColor = '#6f42c1';
|
| 496 |
+
viewButton.style.color = 'white';
|
| 497 |
+
viewButton.title = 'Просмотр';
|
| 498 |
+
viewButton.onclick = (e) => {
|
| 499 |
+
e.stopPropagation();
|
| 500 |
+
openViewer(file);
|
| 501 |
+
};
|
| 502 |
+
fileActionsDiv.appendChild(viewButton);
|
| 503 |
+
}
|
| 504 |
const downloadLink = document.createElement('a');
|
| 505 |
downloadLink.classList.add('download-btn');
|
| 506 |
downloadLink.textContent = 'Скачать';
|
| 507 |
+
downloadLink.href = `/download/${encodeURIComponent(file.filename)}?initData=${encodeURIComponent(userInitData)}`;
|
| 508 |
downloadLink.setAttribute('download', file.filename);
|
| 509 |
downloadLink.title = 'Скачать файл';
|
| 510 |
downloadLink.onclick = (e) => e.stopPropagation();
|
| 511 |
fileActionsDiv.appendChild(downloadLink);
|
| 512 |
+
li.appendChild(fileInfoDiv);
|
| 513 |
li.appendChild(fileActionsDiv);
|
| 514 |
fileListUl.appendChild(li);
|
| 515 |
});
|
|
|
|
| 620 |
uploadStatusDiv.style.color = 'red';
|
| 621 |
} finally {
|
| 622 |
progressBar.style.display = 'none';
|
| 623 |
+
if (currentFiles.length > 0) { // Re-check currentFiles for enabling upload button
|
| 624 |
uploadButton.classList.add('enabled');
|
| 625 |
uploadButton.disabled = false;
|
| 626 |
+
} else {
|
| 627 |
uploadButton.classList.remove('enabled');
|
| 628 |
uploadButton.disabled = true;
|
| 629 |
+
selectedFilesDiv.textContent = 'Файлы не выбраны';
|
|
|
|
|
|
|
| 630 |
}
|
| 631 |
}
|
| 632 |
}
|
| 633 |
|
| 634 |
+
function openViewer(file) {
|
| 635 |
+
modal.style.display = 'block';
|
| 636 |
+
modalContent.innerHTML = '';
|
| 637 |
+
modalCaption.textContent = file.filename;
|
| 638 |
+
const mimeType = file.content_type || '';
|
| 639 |
+
const downloadUrl = `/download/${encodeURIComponent(file.filename)}?initData=${encodeURIComponent(userInitData)}`;
|
| 640 |
+
let element;
|
| 641 |
+
if (mimeType.startsWith('image/')) {
|
| 642 |
+
element = document.createElement('img');
|
| 643 |
+
element.src = downloadUrl;
|
| 644 |
+
element.alt = file.filename;
|
| 645 |
+
} else if (mimeType.startsWith('video/')) {
|
| 646 |
+
element = document.createElement('video');
|
| 647 |
+
element.src = downloadUrl;
|
| 648 |
+
element.controls = true;
|
| 649 |
+
element.autoplay = true;
|
| 650 |
+
} else if (mimeType.startsWith('audio/')) {
|
| 651 |
+
element = document.createElement('audio');
|
| 652 |
+
element.src = downloadUrl;
|
| 653 |
+
element.controls = true;
|
| 654 |
+
element.autoplay = true;
|
| 655 |
+
element.style.padding = '20px';
|
| 656 |
+
}
|
| 657 |
+
if (element) {
|
| 658 |
+
modalContent.appendChild(element);
|
| 659 |
+
if (tg.HapticFeedback) {
|
| 660 |
+
tg.HapticFeedback.impactOccurred('light');
|
| 661 |
+
}
|
| 662 |
+
} else {
|
| 663 |
+
modalCaption.textContent = 'Предпросмотр недоступен для этого типа файла.';
|
| 664 |
+
}
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
function closeViewer() {
|
| 668 |
+
modal.style.display = 'none';
|
| 669 |
+
const mediaElement = modalContent.querySelector('video, audio');
|
| 670 |
+
if (mediaElement) {
|
| 671 |
+
mediaElement.pause();
|
| 672 |
+
mediaElement.src = '';
|
| 673 |
+
}
|
| 674 |
+
modalContent.innerHTML = '';
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
modalCloseBtn.onclick = closeViewer;
|
| 678 |
+
modal.onclick = function(event) {
|
| 679 |
+
if (event.target === modal) {
|
| 680 |
+
closeViewer();
|
| 681 |
+
}
|
| 682 |
+
};
|
| 683 |
+
|
| 684 |
function setupTelegram() {
|
| 685 |
if (!tg || !tg.initData) {
|
| 686 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
|
|
|
| 886 |
.file-table td { font-size: 0.95em; word-break: break-word; }
|
| 887 |
.filename { font-weight: 500; }
|
| 888 |
.filesize, .filedate { color: var(--admin-secondary); font-size: 0.9em; }
|
| 889 |
+
.actions a, .actions button {
|
| 890 |
+
display: inline-block; padding: 6px 10px; margin-left: 8px; border-radius: 6px;
|
| 891 |
+
text-decoration: none; font-size: 0.85em; font-weight: 500; cursor: pointer;
|
| 892 |
+
border: none; transition: opacity 0.2s ease, background-color 0.2s ease;
|
| 893 |
+
vertical-align: middle;
|
| 894 |
}
|
| 895 |
+
.actions a:hover, .actions button:hover { opacity: 0.8; }
|
| 896 |
+
.actions .download-btn { background-color: var(--admin-primary); color: white; }
|
| 897 |
+
.actions .view-btn { background-color: #6f42c1; color: white; }
|
| 898 |
+
|
| 899 |
.no-files { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 900 |
+
|
| 901 |
+
.modal {
|
| 902 |
+
display: none; position: fixed; z-index: 1001;
|
| 903 |
+
left: 0; top: 0; width: 100%; height: 100%;
|
| 904 |
+
overflow: auto; background-color: rgba(0,0,0,0.8);
|
| 905 |
+
backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
|
| 906 |
+
animation: fadeInAdmin 0.3s ease-out;
|
| 907 |
+
}
|
| 908 |
+
@keyframes fadeInAdmin { from { opacity: 0; } to { opacity: 1; } }
|
| 909 |
+
.modal-content {
|
| 910 |
+
margin: auto; display: block; max-width: 90%; max-height: 85%;
|
| 911 |
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 912 |
+
}
|
| 913 |
+
.modal-content img, .modal-content video, .modal-content audio {
|
| 914 |
+
display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; margin: auto;
|
| 915 |
+
background-color: var(--admin-bg);
|
| 916 |
+
}
|
| 917 |
+
.modal-close {
|
| 918 |
+
position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px;
|
| 919 |
+
font-weight: bold; transition: 0.3s; cursor: pointer; z-index: 1002;
|
| 920 |
+
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
| 921 |
+
}
|
| 922 |
+
.modal-close:hover, .modal-close:focus { color: #bbb; text-decoration: none; }
|
| 923 |
+
.modal-caption {
|
| 924 |
+
margin: auto; display: block; width: 80%; max-width: 700px; text-align: center;
|
| 925 |
+
color: #ccc; padding: 10px 0; height: 50px; position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%);
|
| 926 |
+
background: rgba(0,0,0,0.5); border-radius: 8px;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
@media screen and (max-width: 768px) {
|
| 930 |
.file-table { border: 0; box-shadow: none; }
|
| 931 |
.file-table thead { display: none; }
|
|
|
|
| 946 |
<a href="{{ url_for('admin_panel') }}" class="back-link">← Назад к списку пользователей</a>
|
| 947 |
<h1>Файлы пользователя</h1>
|
| 948 |
<div class="user-identifier">{{ user_info.first_name or '' }} {{ user_info.last_name or '' }} (ID: {{ user_id }})</div>
|
| 949 |
+
|
| 950 |
{% if files %}
|
| 951 |
<table class="file-table">
|
| 952 |
<thead>
|
|
|
|
| 959 |
</tr>
|
| 960 |
</thead>
|
| 961 |
<tbody>
|
| 962 |
+
{% for file_item in files|sort(attribute='uploaded_at_ts', reverse=true) %}
|
| 963 |
<tr>
|
| 964 |
+
<td data-label="Имя файла" class="filename">{{ file_item.filename }}</td>
|
| 965 |
+
<td data-label="Размер" class="filesize">{{ file_item.size | filesizeformat if file_item.size else 'N/A' }}</td>
|
| 966 |
+
<td data-label="Дата" class="filedate">{{ file_item.uploaded_at_str or 'N/A' }}</td>
|
| 967 |
+
<td data-label="Тип">{{ file_item.content_type or 'N/A' }}</td>
|
| 968 |
<td data-label="Действия" class="actions">
|
| 969 |
+
{% set mime_type = file_item.content_type or '' %}
|
| 970 |
+
{% if mime_type.startswith('image/') or mime_type.startswith('video/') or mime_type.startswith('audio/') %}
|
| 971 |
+
<button class="view-btn" title="Просмотр"
|
| 972 |
+
onclick="openAdminViewer('{{ user_id }}', '{{ file_item.filename | e }}', '{{ mime_type | e }}')">👁️</button>
|
| 973 |
+
{% endif %}
|
| 974 |
+
<a href="{{ url_for('admin_download_file', user_id=user_id, filename=file_item.filename) }}" class="download-btn">Скачать</a>
|
| 975 |
</td>
|
| 976 |
</tr>
|
| 977 |
{% endfor %}
|
|
|
|
| 981 |
<p class="no-files">У этого пользователя нет загруженных файлов.</p>
|
| 982 |
{% endif %}
|
| 983 |
</div>
|
| 984 |
+
|
| 985 |
+
<div id="adminViewerModal" class="modal">
|
| 986 |
+
<span class="modal-close" id="adminModalCloseBtn">×</span>
|
| 987 |
+
<div id="adminModalContent" class="modal-content">
|
| 988 |
+
</div>
|
| 989 |
+
<div id="adminModalCaption" class="modal-caption"></div>
|
| 990 |
+
</div>
|
| 991 |
+
|
| 992 |
<script>
|
| 993 |
function formatBytes(bytes, decimals = 2) {
|
| 994 |
if (!+bytes) return '0 Bytes'
|
|
|
|
| 998 |
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
| 999 |
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
| 1000 |
}
|
| 1001 |
+
|
| 1002 |
+
const adminModal = document.getElementById('adminViewerModal');
|
| 1003 |
+
const adminModalContent = document.getElementById('adminModalContent');
|
| 1004 |
+
const adminModalCaption = document.getElementById('adminModalCaption');
|
| 1005 |
+
const adminModalCloseBtn = document.getElementById('adminModalCloseBtn');
|
| 1006 |
+
|
| 1007 |
+
function openAdminViewer(userId, filename, mimeType) {
|
| 1008 |
+
adminModal.style.display = 'block';
|
| 1009 |
+
adminModalContent.innerHTML = '';
|
| 1010 |
+
adminModalCaption.textContent = filename;
|
| 1011 |
+
|
| 1012 |
+
const viewUrl = `/admin/view/${encodeURIComponent(userId)}/${encodeURIComponent(filename)}`;
|
| 1013 |
+
let element;
|
| 1014 |
+
|
| 1015 |
+
if (mimeType.startsWith('image/')) {
|
| 1016 |
+
element = document.createElement('img');
|
| 1017 |
+
element.src = viewUrl;
|
| 1018 |
+
element.alt = filename;
|
| 1019 |
+
} else if (mimeType.startsWith('video/')) {
|
| 1020 |
+
element = document.createElement('video');
|
| 1021 |
+
element.src = viewUrl;
|
| 1022 |
+
element.controls = true;
|
| 1023 |
+
element.autoplay = true;
|
| 1024 |
+
} else if (mimeType.startsWith('audio/')) {
|
| 1025 |
+
element = document.createElement('audio');
|
| 1026 |
+
element.src = viewUrl;
|
| 1027 |
+
element.controls = true;
|
| 1028 |
+
element.autoplay = true;
|
| 1029 |
+
element.style.padding = '20px';
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
if (element) {
|
| 1033 |
+
adminModalContent.appendChild(element);
|
| 1034 |
+
} else {
|
| 1035 |
+
adminModalCaption.textContent = 'Предпросмотр недоступен для этого типа файла.';
|
| 1036 |
+
}
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
function closeAdminViewer() {
|
| 1040 |
+
adminModal.style.display = 'none';
|
| 1041 |
+
const mediaElement = adminModalContent.querySelector('video, audio');
|
| 1042 |
+
if (mediaElement) {
|
| 1043 |
+
mediaElement.pause();
|
| 1044 |
+
mediaElement.src = '';
|
| 1045 |
+
}
|
| 1046 |
+
adminModalContent.innerHTML = '';
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
if(adminModalCloseBtn) adminModalCloseBtn.onclick = closeAdminViewer;
|
| 1050 |
+
if(adminModal) {
|
| 1051 |
+
adminModal.onclick = function(event) {
|
| 1052 |
+
if (event.target === adminModal) {
|
| 1053 |
+
closeAdminViewer();
|
| 1054 |
+
}
|
| 1055 |
+
};
|
| 1056 |
+
}
|
| 1057 |
</script>
|
| 1058 |
</body>
|
| 1059 |
</html>
|
|
|
|
| 1224 |
user_info=user_info,
|
| 1225 |
files=files)
|
| 1226 |
|
| 1227 |
+
def _admin_serve_file(user_id, filename, as_attachment):
|
|
|
|
| 1228 |
user_id_str = str(user_id)
|
| 1229 |
+
logging.info(f"Admin serving file '{filename}' for user {user_id_str}, as_attachment={as_attachment}")
|
| 1230 |
api = get_hf_api(write=False)
|
| 1231 |
if not api:
|
| 1232 |
return "Server error: Cannot connect to storage.", 500
|
|
|
|
| 1246 |
force_download=False,
|
| 1247 |
etag_timeout=10
|
| 1248 |
)
|
|
|
|
| 1249 |
content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
| 1250 |
if file_metadata and 'content_type' in file_metadata:
|
| 1251 |
content_type = file_metadata['content_type'] or content_type
|
| 1252 |
return send_file(
|
| 1253 |
local_file_path,
|
| 1254 |
mimetype=content_type,
|
| 1255 |
+
as_attachment=as_attachment,
|
| 1256 |
+
download_name=filename if as_attachment else None
|
| 1257 |
)
|
| 1258 |
except EntryNotFoundError:
|
| 1259 |
+
logging.error(f"Admin serving: File not found on Hugging Face: {path_in_repo}")
|
| 1260 |
return "File not found on storage.", 404
|
| 1261 |
except Exception as e:
|
| 1262 |
+
logging.error(f"Admin serving: Error for file {path_in_repo}: {e}", exc_info=True)
|
| 1263 |
+
return "Server error during file serving.", 500
|
| 1264 |
+
|
| 1265 |
+
@app.route('/admin/download/<user_id>/<path:filename>', methods=['GET'])
|
| 1266 |
+
def admin_download_file(user_id, filename):
|
| 1267 |
+
return _admin_serve_file(user_id, filename, as_attachment=True)
|
| 1268 |
+
|
| 1269 |
+
@app.route('/admin/view/<user_id>/<path:filename>', methods=['GET'])
|
| 1270 |
+
def admin_view_file(user_id, filename):
|
| 1271 |
+
return _admin_serve_file(user_id, filename, as_attachment=False)
|
| 1272 |
+
|
| 1273 |
|
| 1274 |
@app.route('/admin/download_metadata', methods=['POST'])
|
| 1275 |
def admin_trigger_download_metadata():
|