Eluza133 commited on
Commit
76a2595
·
verified ·
1 Parent(s): 28d90f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -59
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(3 ৩৬۰deg); } }
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: 10px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-around; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
100
  #selection-bar.visible { bottom: 10px; }
101
- #selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; }
102
- #move-modal .modal-content { padding: 20px; max-width: 400px; }
 
 
 
 
 
 
 
 
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">{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</p>
 
 
 
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
- <button id="selection-share-btn" class="btn share-btn" onclick="openShareModal()" style="display:none;"><i class="fa-solid fa-share-alt"></i></button>
532
- <button id="selection-download-btn" class="btn download-btn" onclick="downloadSingleSelected()" style="display:none;"><i class="fa-solid fa-download"></i></button>
533
- <button class="btn" style="background: var(--accent);" onclick="showMoveModal()"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
534
- <button class="btn delete-btn" onclick="deleteSelected()"><i class="fa-solid fa-trash-can"></i></button>
535
- <button class="btn" style="background: #555;" onclick="toggleSelectionMode(false)">Отмена</button>
 
 
 
 
 
 
 
 
 
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 firstSelectedId = selectedItems.values().next().value;
699
- const itemElement = document.querySelector(`.item[data-id='${firstSelectedId}']`);
700
- selectionDownloadBtn.style.display = (selectedItems.size === 1 && itemElement?.dataset.type === 'file') ? 'inline-block' : 'none';
701
- selectionShareBtn.style.display = (selectedItems.size === 1 && itemElement?.dataset.type === 'folder') ? 'inline-block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.classList.toggle('selected');
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', () => clearTimeout(longPressTimer));
714
- item.addEventListener('pointerleave', () => clearTimeout(longPressTimer));
715
- item.addEventListener('click', e => {
716
- if (selectionMode) {
717
- haptic.impactOccurred('light'); e.preventDefault(); e.stopPropagation();
718
- item.classList.toggle('selected');
719
- if (selectedItems.has(item.dataset.id)) selectedItems.delete(item.dataset.id); else selectedItems.add(item.dataset.id);
720
- updateSelectionUI();
721
- if (selectedItems.size === 0) toggleSelectionMode(false);
 
 
722
  }
723
- }, true);
 
 
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
- function downloadSingleSelected() {
735
- if (selectedItems.size !== 1) return;
736
- const fileId = selectedItems.values().next().value;
737
- initiateDownload(fileId);
738
- toggleSelectionMode(false);
739
- }
740
- function showMoveModal() { if (selectedItems.size > 0) { haptic.impactOccurred('light'); document.getElementById('move-modal').style.display = 'flex'; }}
 
 
 
 
 
 
 
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
- function deleteSelected() {
748
- if (selectedItems.size === 0) return;
749
- haptic.impactOccurred('medium');
750
- Telegram.WebApp.showConfirm(`Удалить ${selectedItems.size} элемент(ов)?`, ok => {
751
- if (ok) { haptic.impactOccurred('heavy'); performBatchAction('{{ url_for("batch_delete_tma") }}', { item_ids: Array.from(selectedItems) }); }
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
- <p class="item-name">{{ (item.title if item.type == 'note' else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</p>
 
 
 
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