Update app.py
Browse files
app.py
CHANGED
|
@@ -10,6 +10,7 @@ import time
|
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
|
| 12 |
from werkzeug.utils import secure_filename
|
|
|
|
| 13 |
import requests
|
| 14 |
from io import BytesIO
|
| 15 |
import uuid
|
|
@@ -42,9 +43,9 @@ BASE_STYLE = '''
|
|
| 42 |
--text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
| 43 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 44 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 45 |
-
--note-color: #6a5acd; --share-color: #4caf50;
|
| 46 |
}
|
| 47 |
-
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(
|
| 48 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 49 |
html { scroll-behavior: smooth; }
|
| 50 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
@@ -78,7 +79,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 78 |
.item-preview { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 79 |
.item.folder .item-preview { object-fit: contain; font-size: 3.5em; color: var(--folder-color); }
|
| 80 |
.item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
|
| 81 |
-
.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
|
| 82 |
.item-info { font-size: 0.75em; color: var(--text-muted); }
|
| 83 |
.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
|
| 84 |
.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
|
|
@@ -86,6 +87,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 86 |
.file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; }
|
| 87 |
.file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview { font-size: 1.8em; }
|
| 88 |
.file-grid.list-view .item-name-info { flex-grow: 1; }
|
|
|
|
| 89 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; }
|
| 90 |
.modal-content { display: flex; flex-direction: column; max-width: 95%; max-height: 95%; background: var(--card-bg-dark); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; }
|
| 91 |
.modal-main-content { flex-grow: 1; overflow-y: auto; }
|
|
@@ -96,10 +98,18 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 96 |
.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; z-index: 2001;}
|
| 97 |
#progress-container { width: 100%; background: #333; border-radius: 10px; margin: 15px 0; display: none; height: 10px; }
|
| 98 |
#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
|
| 99 |
-
#selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding:
|
| 100 |
#selection-bar.visible { bottom: 10px; }
|
| 101 |
-
#selection-bar .
|
| 102 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
.fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
|
| 104 |
.fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; }
|
| 105 |
.fab:active { transform: scale(0.9); }
|
|
@@ -119,6 +129,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 119 |
.shared-link-info strong { word-break: break-all; }
|
| 120 |
.shared-link-info small { color: var(--text-muted); display: block; }
|
| 121 |
.shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; }
|
|
|
|
| 122 |
'''
|
| 123 |
|
| 124 |
PUBLIC_SHARE_PAGE_HTML = '''
|
|
@@ -191,6 +202,17 @@ def find_node_by_id(filesystem, node_id):
|
|
| 191 |
queue.append((child, current_node))
|
| 192 |
return None, None
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
def add_node(filesystem, parent_id, node_data):
|
| 195 |
parent_node, _ = find_node_by_id(filesystem, parent_id)
|
| 196 |
if parent_node and parent_node.get('type') == 'folder':
|
|
@@ -444,6 +466,7 @@ def auth_via_telegram():
|
|
| 444 |
return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500
|
| 445 |
|
| 446 |
session['telegram_user_id'] = tma_user_id_str
|
|
|
|
| 447 |
display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}"
|
| 448 |
session['telegram_display_name'] = display_name
|
| 449 |
return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')})
|
|
@@ -482,14 +505,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 482 |
<div id="progress-container"><div id="progress-bar"></div></div>
|
| 483 |
<div class="file-grid" id="file-container">
|
| 484 |
{% for item in items %}
|
| 485 |
-
<div class="item {{ item.type }}" data-id="{{ item.id }}" data-type="{{ item.type }}"
|
| 486 |
-
{% if item.type == 'folder' %}
|
| 487 |
-
onclick="window.Telegram.WebApp.HapticFeedback.impactOccurred('light'); window.location.href='{{ url_for('tma_dashboard', folder_id=item.id) }}'"
|
| 488 |
-
{% elif item.type == 'note' %}
|
| 489 |
-
onclick="openNoteModal('{{ item.id }}')"
|
| 490 |
-
{% else %}
|
| 491 |
-
onclick="openModal('{{ hf_file_url_jinja(item.path) if item.file_type not in ['text', 'pdf'] else (url_for('get_text_content_tma', file_id=item.id) if item.file_type == 'text' else hf_file_url_jinja(item.path, True)) }}', '{{ item.file_type }}', '{{ item.id }}')"
|
| 492 |
-
{% endif %}>
|
| 493 |
<div class="item-preview-wrapper">
|
| 494 |
{% if item.type == 'folder' %}
|
| 495 |
<div class="item-preview"><i class="fa-solid fa-folder"></i></div>
|
|
@@ -505,7 +521,10 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 505 |
{% endif %}
|
| 506 |
</div>
|
| 507 |
<div class="item-name-info">
|
| 508 |
-
<p class="item-name">
|
|
|
|
|
|
|
|
|
|
| 509 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 510 |
{% elif item.type == 'note' %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 511 |
</div>
|
|
@@ -527,12 +546,21 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 527 |
</div>
|
| 528 |
|
| 529 |
<div id="selection-bar">
|
| 530 |
-
<span id="selection-count"></span>
|
| 531 |
-
<
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
</div>
|
| 537 |
|
| 538 |
<div class="modal" id="move-modal"><div class="modal-content">
|
|
@@ -544,6 +572,35 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 544 |
<button class="btn" style="background: #555; width: 100%;" onclick="closeMoveModal()">Отмена</button>
|
| 545 |
</div></div>
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
<div class="modal" id="fab-modal"><div class="modal-content">
|
| 548 |
<h4>Добавить в "{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}"</h4>
|
| 549 |
<div class="fab-options">
|
|
@@ -607,12 +664,43 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 607 |
</div>
|
| 608 |
</div></div>
|
| 609 |
|
| 610 |
-
<div class="fab-container"><button id="fab" class="fab"><i class="fa-solid fa-plus"></i></button></div>
|
| 611 |
-
|
| 612 |
<script>
|
| 613 |
window.Telegram.WebApp.ready();
|
| 614 |
window.Telegram.WebApp.expand();
|
| 615 |
const haptic = window.Telegram.WebApp.HapticFeedback;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
async function openModal(srcOrUrl, type, itemId) {
|
| 617 |
if (!srcOrUrl) return;
|
| 618 |
haptic.impactOccurred('light');
|
|
@@ -680,12 +768,11 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 680 |
xhr.addEventListener('error', () => { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert('Ошибка загрузки.'); });
|
| 681 |
xhr.open('POST', this.action, true); xhr.send(formData);
|
| 682 |
});
|
|
|
|
| 683 |
let selectionMode = false; const selectedItems = new Set(); let longPressTimer;
|
| 684 |
const mainContainer = document.getElementById('main-container');
|
| 685 |
const selectionBar = document.getElementById('selection-bar');
|
| 686 |
const selectionCount = document.getElementById('selection-count');
|
| 687 |
-
const selectionDownloadBtn = document.getElementById('selection-download-btn');
|
| 688 |
-
const selectionShareBtn = document.getElementById('selection-share-btn');
|
| 689 |
const allItems = document.querySelectorAll('.item');
|
| 690 |
function toggleSelectionMode(enable) {
|
| 691 |
selectionMode = enable;
|
|
@@ -695,33 +782,70 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 695 |
}
|
| 696 |
function updateSelectionUI() {
|
| 697 |
selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
|
| 698 |
-
const
|
| 699 |
-
const
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
}
|
|
|
|
| 703 |
allItems.forEach(item => {
|
|
|
|
| 704 |
item.addEventListener('pointerdown', e => {
|
| 705 |
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
|
|
| 706 |
longPressTimer = setTimeout(() => {
|
| 707 |
if (!selectionMode) toggleSelectionMode(true);
|
| 708 |
-
item.
|
| 709 |
-
if (selectedItems.has(item.dataset.id)) selectedItems.delete(item.dataset.id); else selectedItems.add(item.dataset.id);
|
| 710 |
-
updateSelectionUI();
|
| 711 |
}, 500);
|
| 712 |
});
|
| 713 |
-
item.addEventListener('pointerup',
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
|
| 722 |
}
|
| 723 |
-
}
|
|
|
|
|
|
|
| 724 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
async function performBatchAction(url, body) {
|
| 726 |
try {
|
| 727 |
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
@@ -731,26 +855,82 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 731 |
} catch (error) { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert('Сетевая ошибка.'); }
|
| 732 |
finally { toggleSelectionMode(false); }
|
| 733 |
}
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
function closeMoveModal() { document.getElementById('move-modal').style.display = 'none'; }
|
| 742 |
function moveSelected() {
|
| 743 |
const destinationId = document.getElementById('folder-destination-select').value;
|
| 744 |
performBatchAction('{{ url_for("batch_move_tma") }}', { item_ids: Array.from(selectedItems), destination_id: destinationId });
|
| 745 |
closeMoveModal();
|
| 746 |
}
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
}
|
|
|
|
| 754 |
async function openNoteModal(noteId = null) {
|
| 755 |
haptic.impactOccurred('light');
|
| 756 |
closeFabModal();
|
|
@@ -877,6 +1057,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 877 |
} else {
|
| 878 |
listEl.innerHTML = '<p>Публичных ссылок для этой папки нет.</p>';
|
| 879 |
}
|
|
|
|
| 880 |
}
|
| 881 |
async function createShareLink() {
|
| 882 |
const folderId = selectedItems.values().next().value;
|
|
@@ -947,6 +1128,9 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 947 |
setView(localStorage.getItem('viewMode') || 'grid');
|
| 948 |
const currentFolderId = '{{ current_folder_id }}';
|
| 949 |
const parentFolderId = '{{ parent_folder_id }}';
|
|
|
|
|
|
|
|
|
|
| 950 |
if (currentFolderId !== 'root') {
|
| 951 |
let backButton = window.Telegram.WebApp.BackButton;
|
| 952 |
backButton.show();
|
|
@@ -977,9 +1161,30 @@ def tma_dashboard():
|
|
| 977 |
current_folder_id = 'root'
|
| 978 |
current_folder, parent_folder = find_node_by_id(user_data['filesystem'], 'root')
|
| 979 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
|
| 982 |
-
items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 983 |
if request.method == 'POST':
|
| 984 |
if not HF_TOKEN_WRITE:
|
| 985 |
flash('Загрузка невозможна: токен для записи не настроен.', 'error')
|
|
@@ -1031,7 +1236,7 @@ def tma_dashboard():
|
|
| 1031 |
|
| 1032 |
all_folders_for_move = get_all_folders(user_data['filesystem'])
|
| 1033 |
|
| 1034 |
-
return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move)
|
| 1035 |
|
| 1036 |
@app.route('/create_folder_tma', methods=['POST'])
|
| 1037 |
def create_folder_tma():
|
|
@@ -1239,6 +1444,92 @@ def batch_move_tma():
|
|
| 1239 |
if errors: return jsonify({'status': 'error', 'message': f'Перемещено {moved_count}. Ошибки: ' + "; ".join(errors)})
|
| 1240 |
return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'})
|
| 1241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
@app.route('/get_text_content_tma/<file_id>')
|
| 1243 |
def get_text_content_tma(file_id):
|
| 1244 |
file_node = get_item_node_for_user(file_id)
|
|
@@ -1649,7 +1940,10 @@ ADMIN_USER_FILES_HTML = '''
|
|
| 1649 |
{% endif %}
|
| 1650 |
</div>
|
| 1651 |
<div class="item-name-info">
|
| 1652 |
-
|
|
|
|
|
|
|
|
|
|
| 1653 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 1654 |
{% elif item.type == 'note' %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 1655 |
</div>
|
|
@@ -1858,8 +2152,13 @@ def admin_user_files(tma_user_id_str):
|
|
| 1858 |
flash('Folder not found!', 'error')
|
| 1859 |
current_folder_id = 'root'
|
| 1860 |
current_folder, _ = find_node_by_id(user_data['filesystem'], 'root')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1861 |
|
| 1862 |
-
items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 1863 |
|
| 1864 |
breadcrumbs = []
|
| 1865 |
temp_id = current_folder_id
|
|
|
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
|
| 12 |
from werkzeug.utils import secure_filename
|
| 13 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 14 |
import requests
|
| 15 |
from io import BytesIO
|
| 16 |
import uuid
|
|
|
|
| 43 |
--text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
| 44 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 45 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 46 |
+
--note-color: #6a5acd; --share-color: #4caf50; --lock-color: #f48fb1;
|
| 47 |
}
|
| 48 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 49 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 50 |
html { scroll-behavior: smooth; }
|
| 51 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
|
|
| 79 |
.item-preview { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
|
| 80 |
.item.folder .item-preview { object-fit: contain; font-size: 3.5em; color: var(--folder-color); }
|
| 81 |
.item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
|
| 82 |
+
.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; display: flex; align-items: center; justify-content: center; gap: 5px; }
|
| 83 |
.item-info { font-size: 0.75em; color: var(--text-muted); }
|
| 84 |
.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
|
| 85 |
.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
|
|
|
|
| 87 |
.file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; }
|
| 88 |
.file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview { font-size: 1.8em; }
|
| 89 |
.file-grid.list-view .item-name-info { flex-grow: 1; }
|
| 90 |
+
.file-grid.list-view .item-name { justify-content: flex-start; }
|
| 91 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; }
|
| 92 |
.modal-content { display: flex; flex-direction: column; max-width: 95%; max-height: 95%; background: var(--card-bg-dark); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; }
|
| 93 |
.modal-main-content { flex-grow: 1; overflow-y: auto; }
|
|
|
|
| 98 |
.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; z-index: 2001;}
|
| 99 |
#progress-container { width: 100%; background: #333; border-radius: 10px; margin: 15px 0; display: none; height: 10px; }
|
| 100 |
#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
|
| 101 |
+
#selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding: 8px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-between; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
|
| 102 |
#selection-bar.visible { bottom: 10px; }
|
| 103 |
+
#selection-bar .selection-info { font-weight: 500; font-size: 0.9em; margin-left: 10px; }
|
| 104 |
+
#selection-bar .actions { display: flex; align-items: center; gap: 5px; }
|
| 105 |
+
#selection-bar .btn-icon { background: none; border: none; color: var(--text-dark); font-size: 1.5em; cursor: pointer; padding: 8px; border-radius: 50%; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; transition: var(--transition); }
|
| 106 |
+
#selection-bar .btn-icon:hover { background: rgba(255,255,255,0.1); }
|
| 107 |
+
#selection-bar .btn-icon:disabled { color: #555; cursor: not-allowed; }
|
| 108 |
+
#selection-more-menu { display: none; position: absolute; bottom: 65px; right: 0; background: var(--card-bg-dark); border-radius: 12px; box-shadow: var(--shadow); z-index: 1001; overflow: hidden; }
|
| 109 |
+
#selection-more-menu button { display: block; width: 100%; padding: 12px 18px; background: none; border: none; color: var(--text-dark); text-align: left; font-size: 1em; cursor: pointer; }
|
| 110 |
+
#selection-more-menu button:hover { background: #2a2a2a; }
|
| 111 |
+
#selection-more-menu button i { margin-right: 10px; }
|
| 112 |
+
#move-modal .modal-content, #rename-modal .modal-content, #lock-modal .modal-content, #unlock-modal .modal-content { padding: 20px; max-width: 400px; }
|
| 113 |
.fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
|
| 114 |
.fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; }
|
| 115 |
.fab:active { transform: scale(0.9); }
|
|
|
|
| 129 |
.shared-link-info strong { word-break: break-all; }
|
| 130 |
.shared-link-info small { color: var(--text-muted); display: block; }
|
| 131 |
.shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; }
|
| 132 |
+
.lock-icon { font-size: 0.8em; color: var(--lock-color); }
|
| 133 |
'''
|
| 134 |
|
| 135 |
PUBLIC_SHARE_PAGE_HTML = '''
|
|
|
|
| 202 |
queue.append((child, current_node))
|
| 203 |
return None, None
|
| 204 |
|
| 205 |
+
def get_node_path(filesystem, node_id):
|
| 206 |
+
path = []
|
| 207 |
+
curr_id = node_id
|
| 208 |
+
while curr_id:
|
| 209 |
+
node, parent = find_node_by_id(filesystem, curr_id)
|
| 210 |
+
if not node: return None
|
| 211 |
+
path.append(node)
|
| 212 |
+
if not parent or node.get('id') == 'root': break
|
| 213 |
+
curr_id = parent.get('id')
|
| 214 |
+
return path[::-1]
|
| 215 |
+
|
| 216 |
def add_node(filesystem, parent_id, node_data):
|
| 217 |
parent_node, _ = find_node_by_id(filesystem, parent_id)
|
| 218 |
if parent_node and parent_node.get('type') == 'folder':
|
|
|
|
| 466 |
return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500
|
| 467 |
|
| 468 |
session['telegram_user_id'] = tma_user_id_str
|
| 469 |
+
session['unlocked_folders'] = []
|
| 470 |
display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}"
|
| 471 |
session['telegram_display_name'] = display_name
|
| 472 |
return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')})
|
|
|
|
| 505 |
<div id="progress-container"><div id="progress-bar"></div></div>
|
| 506 |
<div class="file-grid" id="file-container">
|
| 507 |
{% for item in items %}
|
| 508 |
+
<div class="item {{ item.type }}" data-id="{{ item.id }}" data-type="{{ item.type }}" data-name="{{ item.name if item.type == 'folder' else item.original_filename if item.type == 'file' else item.title }}" data-is-locked="{{ 'true' if item.is_locked else 'false' }}">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
<div class="item-preview-wrapper">
|
| 510 |
{% if item.type == 'folder' %}
|
| 511 |
<div class="item-preview"><i class="fa-solid fa-folder"></i></div>
|
|
|
|
| 521 |
{% endif %}
|
| 522 |
</div>
|
| 523 |
<div class="item-name-info">
|
| 524 |
+
<p class="item-name">
|
| 525 |
+
{% if item.is_locked %}<i class="fa-solid fa-lock lock-icon"></i>{% endif %}
|
| 526 |
+
<span>{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</span>
|
| 527 |
+
</p>
|
| 528 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 529 |
{% elif item.type == 'note' %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 530 |
</div>
|
|
|
|
| 546 |
</div>
|
| 547 |
|
| 548 |
<div id="selection-bar">
|
| 549 |
+
<span id="selection-count" class="selection-info"></span>
|
| 550 |
+
<div class="actions">
|
| 551 |
+
<button id="selection-download-btn" class="btn-icon" title="Скачать"><i class="fa-solid fa-download"></i></button>
|
| 552 |
+
<button id="selection-share-btn" class="btn-icon" title="Поделиться"><i class="fa-solid fa-share-alt"></i></button>
|
| 553 |
+
<button id="selection-move-btn" class="btn-icon" title="Переместить"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
|
| 554 |
+
<button id="selection-delete-btn" class="btn-icon" title="Удалить"><i class="fa-solid fa-trash-can" style="color: var(--delete-color);"></i></button>
|
| 555 |
+
<div style="position: relative;">
|
| 556 |
+
<button id="selection-more-btn" class="btn-icon" title="Еще"><i class="fa-solid fa-ellipsis-vertical"></i></button>
|
| 557 |
+
<div id="selection-more-menu">
|
| 558 |
+
<button id="selection-rename-btn"><i class="fa-solid fa-i-cursor"></i> Переименовать</button>
|
| 559 |
+
<button id="selection-lock-btn"><i class="fa-solid fa-lock"></i> Заблокировать</button>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
<button id="selection-cancel-btn" class="btn-icon" title="Отмена"><i class="fa-solid fa-xmark"></i></button>
|
| 563 |
+
</div>
|
| 564 |
</div>
|
| 565 |
|
| 566 |
<div class="modal" id="move-modal"><div class="modal-content">
|
|
|
|
| 572 |
<button class="btn" style="background: #555; width: 100%;" onclick="closeMoveModal()">Отмена</button>
|
| 573 |
</div></div>
|
| 574 |
|
| 575 |
+
<div class="modal" id="rename-modal"><div class="modal-content">
|
| 576 |
+
<h4>Переименовать</h4>
|
| 577 |
+
<input type="hidden" id="rename-id-input">
|
| 578 |
+
<input type="text" id="rename-name-input" placeholder="Новое имя">
|
| 579 |
+
<button class="btn" style="background: var(--accent); width: 100%; margin-top: 10px;" onclick="submitRename()">Сохранить</button>
|
| 580 |
+
<button class="btn" style="background: #555; width: 100%;" onclick="document.getElementById('rename-modal').style.display = 'none'">Отмена</button>
|
| 581 |
+
</div></div>
|
| 582 |
+
|
| 583 |
+
<div class="modal" id="lock-modal"><div class="modal-content">
|
| 584 |
+
<h4>Заблокировать папку</h4>
|
| 585 |
+
<p>Эта папка и все ее подпапки станут недоступны без пароля.</p>
|
| 586 |
+
<input type="hidden" id="lock-id-input">
|
| 587 |
+
<input type="password" id="lock-password-input" placeholder="Новый пароль" autocomplete="new-password">
|
| 588 |
+
<input type="password" id="lock-password-confirm" placeholder="Подтвердите пароль">
|
| 589 |
+
<button class="btn" style="background: var(--lock-color); width: 100%; margin-top: 10px;" onclick="submitLock()">Заблокировать</button>
|
| 590 |
+
<button class="btn" style="background: #555; width: 100%;" onclick="document.getElementById('lock-modal').style.display = 'none'">Отмена</button>
|
| 591 |
+
</div></div>
|
| 592 |
+
|
| 593 |
+
<div class="modal" id="unlock-modal"><div class="modal-content">
|
| 594 |
+
<h4><i class="fa-solid fa-lock"></i> Папка заблокирована</h4>
|
| 595 |
+
<p>Введите пароль для доступа.</p>
|
| 596 |
+
<input type="hidden" id="unlock-id-input">
|
| 597 |
+
<input type="password" id="unlock-password-input" placeholder="Пароль">
|
| 598 |
+
<button class="btn" style="width: 100%; margin-top: 10px;" onclick="submitUnlock()">Разблокировать</button>
|
| 599 |
+
<button class="btn" style="background: #555; width: 100%;" onclick="document.getElementById('unlock-modal').style.display = 'none'">Отмена</button>
|
| 600 |
+
</div></div>
|
| 601 |
+
|
| 602 |
+
<div class="fab-container"><button id="fab" class="fab"><i class="fa-solid fa-plus"></i></button></div>
|
| 603 |
+
|
| 604 |
<div class="modal" id="fab-modal"><div class="modal-content">
|
| 605 |
<h4>Добавить в "{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}"</h4>
|
| 606 |
<div class="fab-options">
|
|
|
|
| 664 |
</div>
|
| 665 |
</div></div>
|
| 666 |
|
|
|
|
|
|
|
| 667 |
<script>
|
| 668 |
window.Telegram.WebApp.ready();
|
| 669 |
window.Telegram.WebApp.expand();
|
| 670 |
const haptic = window.Telegram.WebApp.HapticFeedback;
|
| 671 |
+
|
| 672 |
+
function handleItemClick(event, itemId, itemType, isLocked) {
|
| 673 |
+
if (selectionMode) {
|
| 674 |
+
toggleItemSelection(event, itemId);
|
| 675 |
+
return;
|
| 676 |
+
}
|
| 677 |
+
haptic.impactOccurred('light');
|
| 678 |
+
if (itemType === 'folder') {
|
| 679 |
+
if (isLocked) {
|
| 680 |
+
event.preventDefault();
|
| 681 |
+
event.stopPropagation();
|
| 682 |
+
openUnlockModal(itemId);
|
| 683 |
+
} else {
|
| 684 |
+
window.location.href = `{{ url_for('tma_dashboard', folder_id='__ID__') }}`.replace('__ID__', itemId);
|
| 685 |
+
}
|
| 686 |
+
} else if (itemType === 'note') {
|
| 687 |
+
openNoteModal(itemId);
|
| 688 |
+
} else {
|
| 689 |
+
const itemElement = document.querySelector(`.item[data-id='${itemId}']`);
|
| 690 |
+
const fileType = itemElement.dataset.type;
|
| 691 |
+
const hfPath = `{{ hf_file_url_jinja('__PATH__') }}`.replace('__PATH__', itemElement.dataset.path);
|
| 692 |
+
let url;
|
| 693 |
+
if (fileType === 'text') {
|
| 694 |
+
url = `{{ url_for('get_text_content_tma', file_id='__ID__') }}`.replace('__ID__', itemId);
|
| 695 |
+
} else if (fileType === 'pdf') {
|
| 696 |
+
url = `{{ hf_file_url_jinja('__PATH__', True) }}`.replace('__PATH__', itemElement.dataset.path);
|
| 697 |
+
} else {
|
| 698 |
+
url = hfPath;
|
| 699 |
+
}
|
| 700 |
+
openModal(url, fileType, itemId);
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
async function openModal(srcOrUrl, type, itemId) {
|
| 705 |
if (!srcOrUrl) return;
|
| 706 |
haptic.impactOccurred('light');
|
|
|
|
| 768 |
xhr.addEventListener('error', () => { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert('Ошибка загрузки.'); });
|
| 769 |
xhr.open('POST', this.action, true); xhr.send(formData);
|
| 770 |
});
|
| 771 |
+
|
| 772 |
let selectionMode = false; const selectedItems = new Set(); let longPressTimer;
|
| 773 |
const mainContainer = document.getElementById('main-container');
|
| 774 |
const selectionBar = document.getElementById('selection-bar');
|
| 775 |
const selectionCount = document.getElementById('selection-count');
|
|
|
|
|
|
|
| 776 |
const allItems = document.querySelectorAll('.item');
|
| 777 |
function toggleSelectionMode(enable) {
|
| 778 |
selectionMode = enable;
|
|
|
|
| 782 |
}
|
| 783 |
function updateSelectionUI() {
|
| 784 |
selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
|
| 785 |
+
const isSingleSelection = selectedItems.size === 1;
|
| 786 |
+
const firstId = isSingleSelection ? selectedItems.values().next().value : null;
|
| 787 |
+
const firstElement = isSingleSelection ? document.querySelector(`.item[data-id='${firstId}']`) : null;
|
| 788 |
+
const firstType = firstElement ? firstElement.dataset.type : null;
|
| 789 |
+
|
| 790 |
+
document.getElementById('selection-download-btn').style.display = (isSingleSelection && firstType === 'file') ? 'flex' : 'none';
|
| 791 |
+
document.getElementById('selection-share-btn').style.display = (isSingleSelection && firstType === 'folder') ? 'flex' : 'none';
|
| 792 |
+
document.getElementById('selection-move-btn').style.display = selectedItems.size > 0 ? 'flex' : 'none';
|
| 793 |
+
document.getElementById('selection-delete-btn').style.display = selectedItems.size > 0 ? 'flex' : 'none';
|
| 794 |
+
|
| 795 |
+
const moreBtn = document.getElementById('selection-more-btn');
|
| 796 |
+
const renameBtn = document.getElementById('selection-rename-btn');
|
| 797 |
+
const lockBtn = document.getElementById('selection-lock-btn');
|
| 798 |
+
|
| 799 |
+
renameBtn.style.display = isSingleSelection ? 'block' : 'none';
|
| 800 |
+
lockBtn.style.display = (isSingleSelection && firstType === 'folder') ? 'block' : 'none';
|
| 801 |
+
|
| 802 |
+
moreBtn.style.display = (renameBtn.style.display === 'block' || lockBtn.style.display === 'block') ? 'flex' : 'none';
|
| 803 |
}
|
| 804 |
+
|
| 805 |
allItems.forEach(item => {
|
| 806 |
+
let touchstartX = 0; let touchstartY = 0; let touchendX = 0; let touchendY = 0;
|
| 807 |
item.addEventListener('pointerdown', e => {
|
| 808 |
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
| 809 |
+
touchstartX = e.clientX; touchstartY = e.clientY;
|
| 810 |
longPressTimer = setTimeout(() => {
|
| 811 |
if (!selectionMode) toggleSelectionMode(true);
|
| 812 |
+
toggleItemSelection(e, item.dataset.id, true);
|
|
|
|
|
|
|
| 813 |
}, 500);
|
| 814 |
});
|
| 815 |
+
item.addEventListener('pointerup', e => {
|
| 816 |
+
clearTimeout(longPressTimer);
|
| 817 |
+
touchendX = e.clientX; touchendY = e.clientY;
|
| 818 |
+
const deltaX = Math.abs(touchendX - touchstartX);
|
| 819 |
+
const deltaY = Math.abs(touchendY - touchstartY);
|
| 820 |
+
if (deltaX < 10 && deltaY < 10) { // It's a click, not a swipe
|
| 821 |
+
if (selectionMode) {
|
| 822 |
+
toggleItemSelection(e, item.dataset.id, false);
|
| 823 |
+
} else {
|
| 824 |
+
handleItemClick(e, item.dataset.id, item.dataset.type, item.dataset.isLocked === 'true');
|
| 825 |
+
}
|
| 826 |
}
|
| 827 |
+
});
|
| 828 |
+
item.addEventListener('pointerleave', () => clearTimeout(longPressTimer));
|
| 829 |
+
item.addEventListener('contextmenu', e => e.preventDefault());
|
| 830 |
});
|
| 831 |
+
|
| 832 |
+
function toggleItemSelection(event, itemId, fromLongPress = false) {
|
| 833 |
+
event.preventDefault();
|
| 834 |
+
event.stopPropagation();
|
| 835 |
+
haptic.impactOccurred('light');
|
| 836 |
+
const item = document.querySelector(`.item[data-id='${itemId}']`);
|
| 837 |
+
item.classList.toggle('selected');
|
| 838 |
+
if (selectedItems.has(itemId)) {
|
| 839 |
+
selectedItems.delete(itemId);
|
| 840 |
+
} else {
|
| 841 |
+
selectedItems.add(itemId);
|
| 842 |
+
}
|
| 843 |
+
updateSelectionUI();
|
| 844 |
+
if (selectedItems.size === 0) {
|
| 845 |
+
toggleSelectionMode(false);
|
| 846 |
+
}
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
async function performBatchAction(url, body) {
|
| 850 |
try {
|
| 851 |
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
|
|
| 855 |
} catch (error) { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert('Сетевая ошибка.'); }
|
| 856 |
finally { toggleSelectionMode(false); }
|
| 857 |
}
|
| 858 |
+
document.getElementById('selection-download-btn').addEventListener('click', () => { if (selectedItems.size === 1) { initiateDownload(selectedItems.values().next().value); toggleSelectionMode(false); }});
|
| 859 |
+
document.getElementById('selection-share-btn').addEventListener('click', () => { if (selectedItems.size === 1) openShareModal(); });
|
| 860 |
+
document.getElementById('selection-move-btn').addEventListener('click', () => { if (selectedItems.size > 0) { haptic.impactOccurred('light'); document.getElementById('move-modal').style.display = 'flex'; }});
|
| 861 |
+
document.getElementById('selection-delete-btn').addEventListener('click', () => { if (selectedItems.size > 0) { haptic.impactOccurred('medium'); Telegram.WebApp.showConfirm(`Удалить ${selectedItems.size} элемент(ов)?`, ok => { if (ok) { haptic.impactOccurred('heavy'); performBatchAction('{{ url_for("batch_delete_tma") }}', { item_ids: Array.from(selectedItems) }); } }); } });
|
| 862 |
+
document.getElementById('selection-cancel-btn').addEventListener('click', () => toggleSelectionMode(false));
|
| 863 |
+
document.getElementById('selection-rename-btn').addEventListener('click', () => openRenameModal());
|
| 864 |
+
document.getElementById('selection-lock-btn').addEventListener('click', () => openLockModal());
|
| 865 |
+
|
| 866 |
+
const moreMenu = document.getElementById('selection-more-menu');
|
| 867 |
+
document.getElementById('selection-more-btn').addEventListener('click', (e) => {
|
| 868 |
+
e.stopPropagation(); moreMenu.style.display = moreMenu.style.display === 'block' ? 'none' : 'block';
|
| 869 |
+
});
|
| 870 |
+
document.addEventListener('click', (e) => { if (moreMenu.style.display === 'block') moreMenu.style.display = 'none'; });
|
| 871 |
+
|
| 872 |
function closeMoveModal() { document.getElementById('move-modal').style.display = 'none'; }
|
| 873 |
function moveSelected() {
|
| 874 |
const destinationId = document.getElementById('folder-destination-select').value;
|
| 875 |
performBatchAction('{{ url_for("batch_move_tma") }}', { item_ids: Array.from(selectedItems), destination_id: destinationId });
|
| 876 |
closeMoveModal();
|
| 877 |
}
|
| 878 |
+
|
| 879 |
+
function openRenameModal() {
|
| 880 |
+
if (selectedItems.size !== 1) return;
|
| 881 |
+
const itemId = selectedItems.values().next().value;
|
| 882 |
+
const itemEl = document.querySelector(`.item[data-id='${itemId}']`);
|
| 883 |
+
document.getElementById('rename-id-input').value = itemId;
|
| 884 |
+
document.getElementById('rename-name-input').value = itemEl.dataset.name;
|
| 885 |
+
document.getElementById('rename-modal').style.display = 'flex';
|
| 886 |
+
moreMenu.style.display = 'none';
|
| 887 |
+
}
|
| 888 |
+
async function submitRename() {
|
| 889 |
+
const itemId = document.getElementById('rename-id-input').value;
|
| 890 |
+
const newName = document.getElementById('rename-name-input').value;
|
| 891 |
+
await performBatchAction('{{ url_for("rename_item_tma") }}', { item_id: itemId, new_name: newName });
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
function openLockModal() {
|
| 895 |
+
if (selectedItems.size !== 1) return;
|
| 896 |
+
const itemId = selectedItems.values().next().value;
|
| 897 |
+
document.getElementById('lock-id-input').value = itemId;
|
| 898 |
+
document.getElementById('lock-modal').style.display = 'flex';
|
| 899 |
+
moreMenu.style.display = 'none';
|
| 900 |
+
}
|
| 901 |
+
async function submitLock() {
|
| 902 |
+
const folderId = document.getElementById('lock-id-input').value;
|
| 903 |
+
const password = document.getElementById('lock-password-input').value;
|
| 904 |
+
const confirm = document.getElementById('lock-password-confirm').value;
|
| 905 |
+
if (password.length < 4) { Telegram.WebApp.showAlert('Пароль должен быть не менее 4 символов.'); return; }
|
| 906 |
+
if (password !== confirm) { Telegram.WebApp.showAlert('Пароли не совпадают.'); return; }
|
| 907 |
+
await performBatchAction('{{ url_for("set_folder_lock_tma") }}', { folder_id: folderId, password: password });
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
function openUnlockModal(folderId) {
|
| 911 |
+
document.getElementById('unlock-id-input').value = folderId;
|
| 912 |
+
document.getElementById('unlock-modal').style.display = 'flex';
|
| 913 |
+
}
|
| 914 |
+
async function submitUnlock() {
|
| 915 |
+
const folderId = document.getElementById('unlock-id-input').value;
|
| 916 |
+
const password = document.getElementById('unlock-password-input').value;
|
| 917 |
+
const modal = document.getElementById('unlock-modal');
|
| 918 |
+
try {
|
| 919 |
+
const response = await fetch('{{ url_for("unlock_folder_tma") }}', {
|
| 920 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 921 |
+
body: JSON.stringify({ folder_id: folderId, password: password })
|
| 922 |
+
});
|
| 923 |
+
const result = await response.json();
|
| 924 |
+
if (result.status === 'success') {
|
| 925 |
+
haptic.notificationOccurred('success');
|
| 926 |
+
window.location.href = `{{ url_for('tma_dashboard') }}?folder_id=${folderId}`;
|
| 927 |
+
} else {
|
| 928 |
+
haptic.notificationOccurred('error');
|
| 929 |
+
Telegram.WebApp.showAlert(result.message || 'Неверный пароль.');
|
| 930 |
+
}
|
| 931 |
+
} catch(e) { Telegram.WebApp.showAlert('Сетевая ошибка.'); }
|
| 932 |
}
|
| 933 |
+
|
| 934 |
async function openNoteModal(noteId = null) {
|
| 935 |
haptic.impactOccurred('light');
|
| 936 |
closeFabModal();
|
|
|
|
| 1057 |
} else {
|
| 1058 |
listEl.innerHTML = '<p>Публичных ссылок для этой папки нет.</p>';
|
| 1059 |
}
|
| 1060 |
+
moreMenu.style.display = 'none';
|
| 1061 |
}
|
| 1062 |
async function createShareLink() {
|
| 1063 |
const folderId = selectedItems.values().next().value;
|
|
|
|
| 1128 |
setView(localStorage.getItem('viewMode') || 'grid');
|
| 1129 |
const currentFolderId = '{{ current_folder_id }}';
|
| 1130 |
const parentFolderId = '{{ parent_folder_id }}';
|
| 1131 |
+
if ('{{ folder_is_locked }}' === 'True') {
|
| 1132 |
+
openUnlockModal(currentFolderId);
|
| 1133 |
+
}
|
| 1134 |
if (currentFolderId !== 'root') {
|
| 1135 |
let backButton = window.Telegram.WebApp.BackButton;
|
| 1136 |
backButton.show();
|
|
|
|
| 1161 |
current_folder_id = 'root'
|
| 1162 |
current_folder, parent_folder = find_node_by_id(user_data['filesystem'], 'root')
|
| 1163 |
|
| 1164 |
+
folder_is_locked = False
|
| 1165 |
+
unlocked_folders = session.get('unlocked_folders', [])
|
| 1166 |
+
if current_folder_id != 'root':
|
| 1167 |
+
path_to_folder = get_node_path(user_data['filesystem'], current_folder_id)
|
| 1168 |
+
if path_to_folder:
|
| 1169 |
+
for node in path_to_folder:
|
| 1170 |
+
if 'lock_hash' in node and node.get('id') not in unlocked_folders:
|
| 1171 |
+
folder_is_locked = True
|
| 1172 |
+
break
|
| 1173 |
+
|
| 1174 |
+
if folder_is_locked and request.method == 'POST':
|
| 1175 |
+
return redirect(url_for('tma_dashboard', folder_id=current_folder_id))
|
| 1176 |
+
|
| 1177 |
+
|
| 1178 |
parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root'
|
| 1179 |
+
|
| 1180 |
+
items_in_folder = []
|
| 1181 |
+
if not folder_is_locked:
|
| 1182 |
+
items = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 1183 |
+
for item in items:
|
| 1184 |
+
item['is_locked'] = 'lock_hash' in item and item.get('type') == 'folder'
|
| 1185 |
+
items_in_folder.append(item)
|
| 1186 |
+
|
| 1187 |
|
|
|
|
| 1188 |
if request.method == 'POST':
|
| 1189 |
if not HF_TOKEN_WRITE:
|
| 1190 |
flash('Загрузка невозможна: токен для записи не настроен.', 'error')
|
|
|
|
| 1236 |
|
| 1237 |
all_folders_for_move = get_all_folders(user_data['filesystem'])
|
| 1238 |
|
| 1239 |
+
return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move, folder_is_locked=folder_is_locked)
|
| 1240 |
|
| 1241 |
@app.route('/create_folder_tma', methods=['POST'])
|
| 1242 |
def create_folder_tma():
|
|
|
|
| 1444 |
if errors: return jsonify({'status': 'error', 'message': f'Перемещено {moved_count}. Ошибки: ' + "; ".join(errors)})
|
| 1445 |
return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'})
|
| 1446 |
|
| 1447 |
+
@app.route('/rename_item_tma', methods=['POST'])
|
| 1448 |
+
def rename_item_tma():
|
| 1449 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1450 |
+
tma_user_id = session['telegram_user_id']
|
| 1451 |
+
data = load_data()
|
| 1452 |
+
user_data = data['users'].get(tma_user_id)
|
| 1453 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1454 |
+
|
| 1455 |
+
item_id = request.json.get('item_id')
|
| 1456 |
+
new_name = request.json.get('new_name', '').strip()
|
| 1457 |
+
|
| 1458 |
+
if not new_name: return jsonify({'status': 'error', 'message': 'Имя не может быть пустым.'}), 400
|
| 1459 |
+
|
| 1460 |
+
node, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 1461 |
+
if not node: return jsonify({'status': 'error', 'message': 'Элемент не найден.'}), 404
|
| 1462 |
+
|
| 1463 |
+
if node['type'] == 'folder':
|
| 1464 |
+
node['name'] = new_name
|
| 1465 |
+
elif node['type'] == 'file':
|
| 1466 |
+
node['original_filename'] = new_name
|
| 1467 |
+
elif node['type'] == 'note':
|
| 1468 |
+
node['title'] = new_name
|
| 1469 |
+
else:
|
| 1470 |
+
return jsonify({'status': 'error', 'message': 'Неподдерживаемый тип элемента.'}), 400
|
| 1471 |
+
|
| 1472 |
+
try:
|
| 1473 |
+
save_data(data)
|
| 1474 |
+
return jsonify({'status': 'success', 'message': 'Переименовано.'})
|
| 1475 |
+
except Exception as e:
|
| 1476 |
+
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1477 |
+
|
| 1478 |
+
@app.route('/set_folder_lock_tma', methods=['POST'])
|
| 1479 |
+
def set_folder_lock_tma():
|
| 1480 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1481 |
+
tma_user_id = session['telegram_user_id']
|
| 1482 |
+
data = load_data()
|
| 1483 |
+
user_data = data['users'].get(tma_user_id)
|
| 1484 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1485 |
+
|
| 1486 |
+
folder_id = request.json.get('folder_id')
|
| 1487 |
+
password = request.json.get('password')
|
| 1488 |
+
|
| 1489 |
+
if not password or len(password) < 4:
|
| 1490 |
+
return jsonify({'status': 'error', 'message': 'Пароль слишком короткий.'}), 400
|
| 1491 |
+
|
| 1492 |
+
folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
|
| 1493 |
+
if not folder_node or folder_node.get('type') != 'folder':
|
| 1494 |
+
return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
|
| 1495 |
+
|
| 1496 |
+
folder_node['lock_hash'] = generate_password_hash(password)
|
| 1497 |
+
|
| 1498 |
+
if folder_id in session.get('unlocked_folders', []):
|
| 1499 |
+
session['unlocked_folders'].remove(folder_id)
|
| 1500 |
+
session.modified = True
|
| 1501 |
+
|
| 1502 |
+
try:
|
| 1503 |
+
save_data(data)
|
| 1504 |
+
return jsonify({'status': 'success', 'message': 'Папка заблокирована.'})
|
| 1505 |
+
except Exception as e:
|
| 1506 |
+
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1507 |
+
|
| 1508 |
+
@app.route('/unlock_folder_tma', methods=['POST'])
|
| 1509 |
+
def unlock_folder_tma():
|
| 1510 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1511 |
+
tma_user_id = session['telegram_user_id']
|
| 1512 |
+
data = load_data()
|
| 1513 |
+
user_data = data['users'].get(tma_user_id)
|
| 1514 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1515 |
+
|
| 1516 |
+
folder_id = request.json.get('folder_id')
|
| 1517 |
+
password = request.json.get('password')
|
| 1518 |
+
|
| 1519 |
+
folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
|
| 1520 |
+
if not folder_node or not folder_node.get('lock_hash'):
|
| 1521 |
+
return jsonify({'status': 'error', 'message': 'Папка не заблокирована.'}), 404
|
| 1522 |
+
|
| 1523 |
+
if check_password_hash(folder_node['lock_hash'], password):
|
| 1524 |
+
if 'unlocked_folders' not in session:
|
| 1525 |
+
session['unlocked_folders'] = []
|
| 1526 |
+
if folder_id not in session['unlocked_folders']:
|
| 1527 |
+
session['unlocked_folders'].append(folder_id)
|
| 1528 |
+
session.modified = True
|
| 1529 |
+
return jsonify({'status': 'success'})
|
| 1530 |
+
else:
|
| 1531 |
+
return jsonify({'status': 'error', 'message': 'Неверный пароль.'}), 403
|
| 1532 |
+
|
| 1533 |
@app.route('/get_text_content_tma/<file_id>')
|
| 1534 |
def get_text_content_tma(file_id):
|
| 1535 |
file_node = get_item_node_for_user(file_id)
|
|
|
|
| 1940 |
{% endif %}
|
| 1941 |
</div>
|
| 1942 |
<div class="item-name-info">
|
| 1943 |
+
<p class="item-name">
|
| 1944 |
+
{% if item.is_locked %}<i class="fa-solid fa-lock lock-icon"></i>{% endif %}
|
| 1945 |
+
<span>{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</span>
|
| 1946 |
+
</p>
|
| 1947 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 1948 |
{% elif item.type == 'note' %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 1949 |
</div>
|
|
|
|
| 2152 |
flash('Folder not found!', 'error')
|
| 2153 |
current_folder_id = 'root'
|
| 2154 |
current_folder, _ = find_node_by_id(user_data['filesystem'], 'root')
|
| 2155 |
+
|
| 2156 |
+
items_in_folder = []
|
| 2157 |
+
items = sorted(current_folder.get('children', []), key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 2158 |
+
for item in items:
|
| 2159 |
+
item['is_locked'] = 'lock_hash' in item and item.get('type') == 'folder'
|
| 2160 |
+
items_in_folder.append(item)
|
| 2161 |
|
|
|
|
| 2162 |
|
| 2163 |
breadcrumbs = []
|
| 2164 |
temp_id = current_folder_id
|