Eluza133 commited on
Commit
28d90f6
·
verified ·
1 Parent(s): 11f1c7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +343 -34
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import flask
2
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
3
  from flask_caching import Cache
@@ -6,7 +7,7 @@ import os
6
  import logging
7
  import threading
8
  import time
9
- from datetime import datetime
10
  from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
11
  from werkzeug.utils import secure_filename
12
  import requests
@@ -41,9 +42,9 @@ BASE_STYLE = '''
41
  --text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
42
  --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
43
  --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
44
- --note-color: #6a5acd;
45
  }
46
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
47
  * { margin: 0; padding: 0; box-sizing: border-box; }
48
  html { scroll-behavior: smooth; }
49
  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; }
@@ -53,7 +54,8 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robo
53
  .view-toggle { display: flex; align-items: center; gap: 5px; }
54
  .view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
55
  .view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
56
- h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; color: var(--text-dark); }
 
57
  .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
58
  .breadcrumbs a { color: var(--accent); text-decoration: none; }
59
  .breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
@@ -64,6 +66,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
64
  .download-btn { background: var(--secondary); }
65
  .delete-btn { background: var(--delete-color); }
66
  .folder-btn { background: var(--folder-color); }
 
67
  .flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); }
68
  .flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); }
69
  .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; }
@@ -77,7 +80,6 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
77
  .item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
78
  .item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
79
  .item-info { font-size: 0.75em; color: var(--text-muted); }
80
- .item-actions { display: none; }
81
  .file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
82
  .file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
83
  .file-grid.list-view .item:hover { transform: translateY(0); }
@@ -97,14 +99,81 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
97
  #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); }
98
  #selection-bar.visible { bottom: 10px; }
99
  #selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; }
100
- #move-modal .modal-content, #fab-modal .modal-content { padding: 20px; max-width: 400px; }
101
  .fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
102
  .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; }
103
  .fab:active { transform: scale(0.9); }
104
  .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; }
105
- .reminder-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
106
- .reminder-item:last-child { border-bottom: none; }
107
- .reminder-item button { background: var(--delete-color); color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  '''
109
 
110
  def find_node_by_id(filesystem, node_id):
@@ -154,7 +223,6 @@ def get_all_folders(filesystem, exclude_ids=None):
154
  if exclude_ids is None:
155
  exclude_ids = set()
156
  folders = []
157
-
158
  def traverse(node, path_prefix):
159
  if node.get('type') == 'folder':
160
  if node.get('id') not in exclude_ids:
@@ -163,7 +231,6 @@ def get_all_folders(filesystem, exclude_ids=None):
163
  new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else ""
164
  for child in node.get('children', []):
165
  traverse(child, new_prefix)
166
-
167
  traverse(filesystem, "")
168
  return sorted(folders, key=lambda x: x['name'].lower())
169
 
@@ -203,15 +270,16 @@ def load_data():
203
  try:
204
  download_db_from_hf()
205
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
206
- if not isinstance(data, dict): data = {'users': {}}
207
  data.setdefault('users', {})
 
208
  for tma_user_id_str, user_data_item in data['users'].items():
209
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
210
  user_data_item.setdefault('reminders', [])
211
  return data
212
  except Exception as e:
213
  logging.error(f"Error loading data: {e}")
214
- return {'users': {}}
215
 
216
  def save_data(data):
217
  try:
@@ -234,17 +302,17 @@ def upload_db_to_hf():
234
  def download_db_from_hf():
235
  if not HF_TOKEN_READ:
236
  if not os.path.exists(DATA_FILE):
237
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
238
  return
239
  try:
240
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
241
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
242
  if not os.path.exists(DATA_FILE):
243
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
244
  except Exception as e:
245
  logging.error(f"Error downloading database: {e}")
246
  if not os.path.exists(DATA_FILE):
247
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
248
 
249
  def periodic_backup():
250
  while True:
@@ -460,7 +528,8 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
460
 
461
  <div id="selection-bar">
462
  <span id="selection-count"></span>
463
- <button id="selection-download-btn" class="btn download-btn" onclick="downloadSingleSelected()" style="display:none;"><i class="fa-solid fa-download"></i> Скачать</button>
 
464
  <button class="btn" style="background: var(--accent);" onclick="showMoveModal()"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
465
  <button class="btn delete-btn" onclick="deleteSelected()"><i class="fa-solid fa-trash-can"></i></button>
466
  <button class="btn" style="background: #555;" onclick="toggleSelectionMode(false)">Отмена</button>
@@ -476,20 +545,29 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
476
  </div></div>
477
 
478
  <div class="modal" id="fab-modal"><div class="modal-content">
479
- <h4>Действия</h4>
480
- <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}">
 
 
 
 
 
 
 
 
 
 
 
481
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
482
  <input type="file" name="files" id="file-input" multiple required onchange="document.getElementById('upload-btn-modal').click()">
483
- <label for="file-input" class="btn" style="width:100%; margin-bottom: 10px; background: var(--secondary);"><i class="fa-solid fa-upload"></i> Загрузить файлы</label>
484
- <button type="submit" id="upload-btn-modal" style="display:none;"></button>
485
  </form>
486
- <button class="btn" style="width:100%; margin-bottom: 10px; background: var(--note-color);" onclick="openNoteModal()"><i class="fa-solid fa-note-sticky"></i> Создать заметку</button>
487
- <form method="POST" action="{{ url_for('create_folder_tma') }}">
488
  <input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
489
  <input type="text" name="folder_name" placeholder="Имя новой папки" required>
490
- <button type="submit" class="btn folder-btn" style="width:100%"><i class="fa-solid fa-folder-plus"></i> Создать папку</button>
491
  </form>
492
- <button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()">Отмена</button>
493
  </div></div>
494
 
495
  <div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
@@ -515,6 +593,20 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
515
  </div>
516
  </div></div>
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  <div class="fab-container"><button id="fab" class="fab"><i class="fa-solid fa-plus"></i></button></div>
519
 
520
  <script>
@@ -593,6 +685,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
593
  const selectionBar = document.getElementById('selection-bar');
594
  const selectionCount = document.getElementById('selection-count');
595
  const selectionDownloadBtn = document.getElementById('selection-download-btn');
 
596
  const allItems = document.querySelectorAll('.item');
597
  function toggleSelectionMode(enable) {
598
  selectionMode = enable;
@@ -604,9 +697,8 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
604
  selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
605
  const firstSelectedId = selectedItems.values().next().value;
606
  const itemElement = document.querySelector(`.item[data-id='${firstSelectedId}']`);
607
- if (selectedItems.size === 1 && itemElement && itemElement.dataset.type === 'file') {
608
- selectionDownloadBtn.style.display = 'inline-block';
609
- } else { selectionDownloadBtn.style.display = 'none'; }
610
  }
611
  allItems.forEach(item => {
612
  item.addEventListener('pointerdown', e => {
@@ -755,6 +847,78 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
755
  }
756
  });
757
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  document.getElementById('reminders-btn').addEventListener('click', openRemindersModal);
759
  const gridViewBtn = document.getElementById('grid-view-btn');
760
  const listViewBtn = document.getElementById('list-view-btn');
@@ -771,8 +935,14 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
771
  const fab = document.getElementById('fab');
772
  const fabModal = document.getElementById('fab-modal');
773
  fab.addEventListener('click', () => { haptic.impactOccurred('medium'); fabModal.style.display = 'flex'; });
774
- function closeFabModal() { fabModal.style.display = 'none'; }
 
 
 
775
  fabModal.addEventListener('click', e => { if (e.target.id === 'fab-modal') closeFabModal(); });
 
 
 
776
  document.addEventListener('DOMContentLoaded', () => {
777
  setView(localStorage.getItem('viewMode') || 'grid');
778
  const currentFolderId = '{{ current_folder_id }}';
@@ -967,11 +1137,8 @@ def batch_download_tma():
967
  if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
968
  file_ids_str = request.args.get('file_ids')
969
  if not file_ids_str: return Response("No file IDs provided", 400)
970
-
971
  file_ids = file_ids_str.split(',')
972
-
973
  temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
974
-
975
  try:
976
  with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf:
977
  for file_id in file_ids:
@@ -1206,6 +1373,148 @@ def tma_logout():
1206
  flash('Вы вышли из сессии приложения.')
1207
  return redirect(url_for('tma_entry_page'))
1208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  ADMIN_LOGIN_HTML = '''
1210
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
1211
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -1703,11 +2012,11 @@ if __name__ == '__main__':
1703
  download_db_from_hf()
1704
  else:
1705
  if not os.path.exists(DATA_FILE):
1706
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
1707
 
1708
  if HF_TOKEN_WRITE:
1709
  threading.Thread(target=periodic_backup, daemon=True).start()
1710
 
1711
  threading.Thread(target=check_reminders, daemon=True).start()
1712
 
1713
- app.run(debug=False, host='0.0.0.0', port=7860)
 
1
+
2
  import flask
3
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
4
  from flask_caching import Cache
 
7
  import logging
8
  import threading
9
  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
 
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; }
 
54
  .view-toggle { display: flex; align-items: center; gap: 5px; }
55
  .view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
56
  .view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
57
+ h2, h3, h4, h5 { color: var(--text-dark); }
58
+ h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; }
59
  .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
60
  .breadcrumbs a { color: var(--accent); text-decoration: none; }
61
  .breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
 
66
  .download-btn { background: var(--secondary); }
67
  .delete-btn { background: var(--delete-color); }
68
  .folder-btn { background: var(--folder-color); }
69
+ .share-btn { background: var(--share-color); }
70
  .flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); }
71
  .flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); }
72
  .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; }
 
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; }
85
  .file-grid.list-view .item:hover { transform: translateY(0); }
 
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); }
106
  .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; }
107
+ #fab-modal .modal-content { padding: 20px; max-width: 400px; background: var(--card-bg-dark); text-align: center; }
108
+ .fab-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; }
109
+ .fab-option { display: flex; flex-direction: column; align-items: center; justify-content: center; background: #2a2a2a; border-radius: 12px; padding: 15px; cursor: pointer; transition: var(--transition); text-decoration:none; color: var(--text-dark); }
110
+ .fab-option:hover { background: #333; transform: translateY(-3px); }
111
+ .fab-option i { font-size: 2em; margin-bottom: 8px; }
112
+ #fab-option-upload i { color: var(--secondary); }
113
+ #fab-option-note i { color: var(--note-color); }
114
+ #fab-option-folder i { color: var(--folder-color); }
115
+ #create-folder-form { display: none; margin-top: 15px; }
116
+ .shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
117
+ .shared-link-item:last-child { border-bottom: none; }
118
+ .shared-link-info { text-align: left; }
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 = '''
125
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
126
+ <title>Общая папка: {{ folder.name }}</title>
127
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
128
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
129
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
130
+ <style>''' + BASE_STYLE + '''
131
+ body { padding-bottom: 30px; }
132
+ .public-header { padding: 15px; text-align: center; border-bottom: 1px solid #333; margin-bottom: 20px; }
133
+ .item { cursor: default; }
134
+ .item .download-icon { position: absolute; top: 10px; right: 10px; font-size: 1.2em; color: var(--text-muted); cursor: pointer; transition: var(--transition); }
135
+ .item .download-icon:hover { color: var(--secondary); }
136
+ .list-view .item .download-icon { position: static; margin-left: auto; padding: 5px 10px; }
137
+ </style></head><body>
138
+ <div class="public-header">
139
+ <h1>Общая папка</h1>
140
+ <h2>{{ folder.name }}</h2>
141
+ <p style="color: var(--text-muted);">Автор: {{ user.first_name or user.telegram_username }}</p>
142
+ </div>
143
+ <div class="container" style="padding-top: 15px;">
144
+ <div class="file-grid list-view">
145
+ {% for item in items %}
146
+ <div class="item {{ item.type }}">
147
+ <div class="item-preview-wrapper">
148
+ {% if item.type == 'folder' %}
149
+ <a href="{{ url_for('shared_folder_view', link_id=link.id, subfolder_id=item.id) }}" style="text-decoration: none; color: inherit;">
150
+ <div class="item-preview"><i class="fa-solid fa-folder"></i></div>
151
+ </a>
152
+ {% elif item.type == 'note' %}
153
+ <div class="item-preview"><i class="fa-solid fa-note-sticky"></i></div>
154
+ {% elif item.file_type == 'image' %}<div class="item-preview" style="background-image: url({{ hf_file_url_jinja(item.path) }}); background-size: cover; background-position: center;"></div>
155
+ {% else %}<div class="item-preview" style="font-size: 1.8em; display: flex; align-items: center; justify-content: center;"><i class="fa-solid fa-file"></i></div>{% endif %}
156
+ </div>
157
+ <div class="item-name-info">
158
+ <p class="item-name">
159
+ {% if item.type == 'folder' %}
160
+ <a href="{{ url_for('shared_folder_view', link_id=link.id, subfolder_id=item.id) }}" style="text-decoration: none; color: inherit;">{{ item.name }}</a>
161
+ {% else %}
162
+ {{ item.title if item.type == 'note' else item.original_filename }}
163
+ {% endif %}
164
+ </p>
165
+ <p class="item-info">{% if item.type == 'file' %}{{ item.upload_date }}{% elif item.type == 'note' %}{{ item.modified_date }}{% endif %}</p>
166
+ </div>
167
+ {% if item.type != 'folder' %}
168
+ <a href="{{ url_for('public_download_via_link', link_id=link.id, item_id=item.id) }}" class="download-icon" title="Скачать">
169
+ <i class="fa-solid fa-download"></i>
170
+ </a>
171
+ {% endif %}
172
+ </div>
173
+ {% endfor %}
174
+ {% if not items %}<p>Эта папка пуста.</p>{% endif %}
175
+ </div>
176
+ </div></body></html>
177
  '''
178
 
179
  def find_node_by_id(filesystem, node_id):
 
223
  if exclude_ids is None:
224
  exclude_ids = set()
225
  folders = []
 
226
  def traverse(node, path_prefix):
227
  if node.get('type') == 'folder':
228
  if node.get('id') not in exclude_ids:
 
231
  new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else ""
232
  for child in node.get('children', []):
233
  traverse(child, new_prefix)
 
234
  traverse(filesystem, "")
235
  return sorted(folders, key=lambda x: x['name'].lower())
236
 
 
270
  try:
271
  download_db_from_hf()
272
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
273
+ if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}}
274
  data.setdefault('users', {})
275
+ data.setdefault('shared_links', {})
276
  for tma_user_id_str, user_data_item in data['users'].items():
277
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
278
  user_data_item.setdefault('reminders', [])
279
  return data
280
  except Exception as e:
281
  logging.error(f"Error loading data: {e}")
282
+ return {'users': {}, 'shared_links': {}}
283
 
284
  def save_data(data):
285
  try:
 
302
  def download_db_from_hf():
303
  if not HF_TOKEN_READ:
304
  if not os.path.exists(DATA_FILE):
305
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
306
  return
307
  try:
308
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
309
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
310
  if not os.path.exists(DATA_FILE):
311
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
312
  except Exception as e:
313
  logging.error(f"Error downloading database: {e}")
314
  if not os.path.exists(DATA_FILE):
315
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
316
 
317
  def periodic_backup():
318
  while True:
 
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>
 
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">
550
+ <label for="file-input" class="fab-option" id="fab-option-upload">
551
+ <i class="fa-solid fa-upload"></i><span>Файлы</span>
552
+ </label>
553
+ <div class="fab-option" id="fab-option-note" onclick="openNoteModal()">
554
+ <i class="fa-solid fa-note-sticky"></i><span>Заметку</span>
555
+ </div>
556
+ <div class="fab-option" id="fab-option-folder">
557
+ <i class="fa-solid fa-folder-plus"></i><span>Папку</span>
558
+ </div>
559
+ </div>
560
+ <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
561
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
562
  <input type="file" name="files" id="file-input" multiple required onchange="document.getElementById('upload-btn-modal').click()">
563
+ <button type="submit" id="upload-btn-modal"></button>
 
564
  </form>
565
+ <form method="POST" action="{{ url_for('create_folder_tma') }}" id="create-folder-form">
 
566
  <input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
567
  <input type="text" name="folder_name" placeholder="Имя новой папки" required>
568
+ <button type="submit" class="btn folder-btn" style="width:100%">Создать</button>
569
  </form>
570
+ <button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()">Закрыть</button>
571
  </div></div>
572
 
573
  <div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
 
593
  </div>
594
  </div></div>
595
 
596
+ <div class="modal" id="share-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
597
+ <h4>Поделиться папкой</h4>
598
+ <div id="existing-links-list" style="max-height: 30vh; overflow-y: auto; margin-bottom: 15px;"></div>
599
+ <h5 style="margin-top: 15px;">Создать новую ссылку</h5>
600
+ <input type="text" id="share-link-name" placeholder="Название ссылки (необязательно)">
601
+ <select id="share-link-duration">
602
+ <option value="1">1 час</option><option value="24">24 часа</option><option value="168">7 дней</option><option value="0">Всегда</option>
603
+ </select>
604
+ <div style="display: flex; gap: 10px; margin-top: 15px;">
605
+ <button class="btn share-btn" style="flex-grow: 1;" onclick="createShareLink()">Создать</button>
606
+ <button class="btn" style="background: #555; flex-grow: 1;" onclick="closeShareModal()">Закрыть</button>
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>
 
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;
 
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 => {
 
847
  }
848
  });
849
  }
850
+ function closeShareModal() { document.getElementById('share-modal').style.display = 'none'; }
851
+ async function openShareModal() {
852
+ if (selectedItems.size !== 1) return;
853
+ haptic.impactOccurred('light');
854
+ const folderId = selectedItems.values().next().value;
855
+ const listEl = document.getElementById('existing-links-list');
856
+ listEl.innerHTML = '<div class="loading-spinner"></div>';
857
+ document.getElementById('share-modal').style.display = 'flex';
858
+ const response = await fetch(`{{ url_for('get_public_links', folder_id='FOLDER_ID') }}`.replace('FOLDER_ID', folderId));
859
+ const data = await response.json();
860
+ listEl.innerHTML = '';
861
+ if (data.status === 'success' && data.links.length > 0) {
862
+ data.links.forEach(link => {
863
+ const el = document.createElement('div');
864
+ el.className = 'shared-link-item';
865
+ const expiration = link.expires_at ? new Date(link.expires_at).toLocaleString() : 'Никогда';
866
+ el.innerHTML = `
867
+ <div class="shared-link-info">
868
+ <strong>${link.name || 'Безымянная ссылка'}</strong>
869
+ <small>Истекает: ${expiration}</small>
870
+ </div>
871
+ <div class="shared-link-actions">
872
+ <button onclick="copyToClipboard('${link.url}')" title="Копировать"><i class="fa-solid fa-copy"></i></button>
873
+ <button onclick="deleteShareLink('${link.id}')" title="Удалить"><i class="fa-solid fa-trash"></i></button>
874
+ </div>`;
875
+ listEl.appendChild(el);
876
+ });
877
+ } else {
878
+ listEl.innerHTML = '<p>Публичных ссылок для этой папки нет.</p>';
879
+ }
880
+ }
881
+ async function createShareLink() {
882
+ const folderId = selectedItems.values().next().value;
883
+ const name = document.getElementById('share-link-name').value;
884
+ const duration_hours = document.getElementById('share-link-duration').value;
885
+ const response = await fetch('{{ url_for("create_public_link") }}', {
886
+ method: 'POST', headers: {'Content-Type': 'application/json'},
887
+ body: JSON.stringify({ folder_id: folderId, name: name, duration_hours: parseInt(duration_hours) })
888
+ });
889
+ const result = await response.json();
890
+ if (result.status === 'success') {
891
+ haptic.notificationOccurred('success');
892
+ openShareModal();
893
+ copyToClipboard(result.url);
894
+ Telegram.WebApp.showAlert('Ссылка создана и скопирована!');
895
+ } else {
896
+ haptic.notificationOccurred('error');
897
+ Telegram.WebApp.showAlert(result.message || 'Ошибка создания ссылки.');
898
+ }
899
+ }
900
+ async function deleteShareLink(linkId) {
901
+ Telegram.WebApp.showConfirm('Удалить эту публичную ссылку?', async (ok) => {
902
+ if(ok) {
903
+ haptic.impactOccurred('heavy');
904
+ const response = await fetch('{{ url_for("delete_public_link") }}', {
905
+ method: 'POST', headers: {'Content-Type': 'application/json'},
906
+ body: JSON.stringify({ link_id: linkId })
907
+ });
908
+ const result = await response.json();
909
+ if (result.status === 'success') { openShareModal(); }
910
+ else { Telegram.WebApp.showAlert('Ошибка удаления.'); }
911
+ }
912
+ });
913
+ }
914
+ function copyToClipboard(text) {
915
+ navigator.clipboard.writeText(text).then(() => {
916
+ haptic.notificationOccurred('success');
917
+ Telegram.WebApp.showAlert('Скопировано!');
918
+ }, () => {
919
+ haptic.notificationOccurred('error');
920
+ });
921
+ }
922
  document.getElementById('reminders-btn').addEventListener('click', openRemindersModal);
923
  const gridViewBtn = document.getElementById('grid-view-btn');
924
  const listViewBtn = document.getElementById('list-view-btn');
 
935
  const fab = document.getElementById('fab');
936
  const fabModal = document.getElementById('fab-modal');
937
  fab.addEventListener('click', () => { haptic.impactOccurred('medium'); fabModal.style.display = 'flex'; });
938
+ function closeFabModal() {
939
+ fabModal.style.display = 'none';
940
+ document.getElementById('create-folder-form').style.display = 'none';
941
+ }
942
  fabModal.addEventListener('click', e => { if (e.target.id === 'fab-modal') closeFabModal(); });
943
+ document.getElementById('fab-option-folder').addEventListener('click', () => {
944
+ document.getElementById('create-folder-form').style.display = 'block';
945
+ });
946
  document.addEventListener('DOMContentLoaded', () => {
947
  setView(localStorage.getItem('viewMode') || 'grid');
948
  const currentFolderId = '{{ current_folder_id }}';
 
1137
  if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
1138
  file_ids_str = request.args.get('file_ids')
1139
  if not file_ids_str: return Response("No file IDs provided", 400)
 
1140
  file_ids = file_ids_str.split(',')
 
1141
  temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
 
1142
  try:
1143
  with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf:
1144
  for file_id in file_ids:
 
1373
  flash('Вы вышли из сессии приложения.')
1374
  return redirect(url_for('tma_entry_page'))
1375
 
1376
+ @app.route('/create_public_link', methods=['POST'])
1377
+ def create_public_link():
1378
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1379
+ tma_user_id = session['telegram_user_id']
1380
+ data = load_data()
1381
+ user_data = data['users'].get(tma_user_id)
1382
+ if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
1383
+
1384
+ payload = request.json
1385
+ folder_id = payload.get('folder_id')
1386
+ name = payload.get('name')
1387
+ duration_hours = payload.get('duration_hours', 0)
1388
+
1389
+ folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
1390
+ if not folder_node or folder_node.get('type') != 'folder':
1391
+ return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
1392
+
1393
+ now = datetime.now(pytz.utc)
1394
+ expires_at = None
1395
+ if duration_hours > 0:
1396
+ expires_at = now + timedelta(hours=duration_hours)
1397
+ expires_at_iso = expires_at.isoformat()
1398
+ else:
1399
+ expires_at_iso = None
1400
+
1401
+ link_id = uuid.uuid4().hex
1402
+ link_data = {
1403
+ 'id': link_id,
1404
+ 'user_id': tma_user_id,
1405
+ 'folder_id': folder_id,
1406
+ 'name': name,
1407
+ 'created_at': now.isoformat(),
1408
+ 'expires_at': expires_at_iso
1409
+ }
1410
+ data['shared_links'][link_id] = link_data
1411
+
1412
+ folder_node.setdefault('public_links', []).append(link_id)
1413
+
1414
+ try:
1415
+ save_data(data)
1416
+ public_url = url_for('shared_folder_view', link_id=link_id, _external=True)
1417
+ return jsonify({'status': 'success', 'url': public_url})
1418
+ except Exception as e:
1419
+ return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1420
+
1421
+ @app.route('/delete_public_link', methods=['POST'])
1422
+ def delete_public_link():
1423
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1424
+ tma_user_id = session['telegram_user_id']
1425
+ data = load_data()
1426
+
1427
+ link_id = request.json.get('link_id')
1428
+ link_data = data['shared_links'].get(link_id)
1429
+
1430
+ if not link_data or link_data.get('user_id') != tma_user_id:
1431
+ return jsonify({'status': 'error', 'message': 'Ссылка не найдена или нет доступа.'}), 404
1432
+
1433
+ folder_id = link_data.get('folder_id')
1434
+ user_data = data['users'].get(tma_user_id)
1435
+ if user_data:
1436
+ folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
1437
+ if folder and 'public_links' in folder:
1438
+ folder['public_links'] = [l for l in folder['public_links'] if l != link_id]
1439
+
1440
+ del data['shared_links'][link_id]
1441
+
1442
+ try:
1443
+ save_data(data)
1444
+ return jsonify({'status': 'success'})
1445
+ except Exception as e:
1446
+ return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1447
+
1448
+ @app.route('/get_public_links/<folder_id>')
1449
+ def get_public_links(folder_id):
1450
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1451
+ tma_user_id = session['telegram_user_id']
1452
+ data = load_data()
1453
+ user_data = data['users'].get(tma_user_id)
1454
+ if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
1455
+
1456
+ folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
1457
+ if not folder: return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
1458
+
1459
+ link_ids = folder.get('public_links', [])
1460
+ links_details = []
1461
+ for link_id in link_ids:
1462
+ link_data = data['shared_links'].get(link_id)
1463
+ if link_data:
1464
+ link_data['url'] = url_for('shared_folder_view', link_id=link_id, _external=True)
1465
+ links_details.append(link_data)
1466
+
1467
+ return jsonify({'status': 'success', 'links': links_details})
1468
+
1469
+ @app.route('/shared/<link_id>')
1470
+ @app.route('/shared/<link_id>/<subfolder_id>')
1471
+ def shared_folder_view(link_id, subfolder_id=None):
1472
+ data = load_data()
1473
+ link_data = data['shared_links'].get(link_id)
1474
+
1475
+ if not link_data: return "Ссылка недействительна.", 404
1476
+
1477
+ if link_data.get('expires_at'):
1478
+ expires_at = datetime.fromisoformat(link_data['expires_at'])
1479
+ if datetime.now(pytz.utc) > expires_at:
1480
+ return "Срок действия ссылки истек.", 410
1481
+
1482
+ user_id = link_data['user_id']
1483
+ user_data = data['users'].get(user_id)
1484
+ if not user_data: return "Владелец не найден.", 404
1485
+
1486
+ folder_id_to_show = subfolder_id if subfolder_id else link_data['folder_id']
1487
+ folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id_to_show)
1488
+
1489
+ if not folder_node or folder_node.get('type') != 'folder':
1490
+ return "Папка не найдена.", 404
1491
+
1492
+ items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
1493
+
1494
+ return render_template_string(PUBLIC_SHARE_PAGE_HTML, folder=folder_node, items=items_in_folder, user=user_data, link=link_data, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
1495
+
1496
+ @app.route('/public_download/<link_id>/<item_id>')
1497
+ def public_download_via_link(link_id, item_id):
1498
+ data = load_data()
1499
+ link_data = data['shared_links'].get(link_id)
1500
+ if not link_data: return Response("Ссылка неде��ствительна.", status=404)
1501
+
1502
+ if link_data.get('expires_at'):
1503
+ expires_at = datetime.fromisoformat(link_data['expires_at'])
1504
+ if datetime.now(pytz.utc) > expires_at:
1505
+ return Response("Срок действия ссылки истек.", status=410)
1506
+
1507
+ user_id = link_data['user_id']
1508
+ user_data = data['users'].get(user_id)
1509
+ if not user_data: return Response("Владелец не найден.", status=404)
1510
+
1511
+ item_node, _ = find_node_by_id(user_data['filesystem'], item_id)
1512
+ if not item_node: return Response("Элемент не найден.", status=404)
1513
+
1514
+ token = uuid.uuid4().hex
1515
+ cache.set(f"download_token_{token}", item_node, timeout=300)
1516
+ return redirect(url_for('public_download', token=token))
1517
+
1518
  ADMIN_LOGIN_HTML = '''
1519
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
1520
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
2012
  download_db_from_hf()
2013
  else:
2014
  if not os.path.exists(DATA_FILE):
2015
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
2016
 
2017
  if HF_TOKEN_WRITE:
2018
  threading.Thread(target=periodic_backup, daemon=True).start()
2019
 
2020
  threading.Thread(target=check_reminders, daemon=True).start()
2021
 
2022
+ app.run(debug=False, host='0.0.0.0', port=7860)