Update app.py
Browse files
app.py
CHANGED
|
@@ -42,9 +42,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,13 +78,15 @@ 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; }
|
| 85 |
.file-grid.list-view .item:hover { transform: translateY(0); }
|
| 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; }
|
|
@@ -104,14 +106,16 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 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:
|
| 108 |
-
.fab-options { display: grid; grid-template-columns: repeat(
|
| 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; }
|
|
@@ -121,7 +125,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 121 |
.shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; }
|
| 122 |
'''
|
| 123 |
|
| 124 |
-
|
| 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>
|
|
@@ -176,6 +180,110 @@ body { padding-bottom: 30px; }
|
|
| 176 |
</div></body></html>
|
| 177 |
'''
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
def find_node_by_id(filesystem, node_id):
|
| 180 |
if not filesystem: return None, None
|
| 181 |
if filesystem.get('id') == node_id:
|
|
@@ -238,7 +346,7 @@ def count_items_recursive(node):
|
|
| 238 |
if not node or not isinstance(node, dict):
|
| 239 |
return 0
|
| 240 |
count = 0
|
| 241 |
-
if node.get('type') in ['file', 'note']:
|
| 242 |
count += 1
|
| 243 |
if node.get('type') == 'folder' and 'children' in node:
|
| 244 |
for child in node.get('children', []):
|
|
@@ -458,10 +566,20 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 458 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 459 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 460 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 461 |
-
<style>''' + BASE_STYLE + '''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
<div class="app-header">
|
| 463 |
<div class="user-info">{{ display_name }}</div>
|
| 464 |
<div class="view-toggle">
|
|
|
|
| 465 |
<button id="reminders-btn" title="Напоминания"><i class="fa-solid fa-bell"></i></button>
|
| 466 |
<button id="grid-view-btn" title="Сетка"><i class="fa fa-th-large"></i></button>
|
| 467 |
<button id="list-view-btn" title="Список"><i class="fa fa-bars"></i></button>
|
|
@@ -487,14 +605,18 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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 |
-
|
| 496 |
-
{% elif item.type == '
|
| 497 |
-
|
| 498 |
{% elif item.type == 'file' %}
|
| 499 |
{% if item.file_type == 'image' %}<img class="item-preview" src="{{ hf_file_url_jinja(item.path) }}" loading="lazy">
|
| 500 |
{% elif item.file_type == 'video' %}<video class="item-preview" preload="metadata" muted loading="lazy"><source src="{{ hf_file_url_jinja(item.path, True) }}#t=0.5"></video>
|
|
@@ -505,9 +627,9 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 505 |
{% endif %}
|
| 506 |
</div>
|
| 507 |
<div class="item-name-info">
|
| 508 |
-
<p class="item-name">{{ (item.title if item.type
|
| 509 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 510 |
-
{% elif item.type
|
| 511 |
</div>
|
| 512 |
</div>
|
| 513 |
{% endfor %}
|
|
@@ -528,6 +650,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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>
|
|
@@ -556,6 +679,12 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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 }}">
|
|
@@ -581,6 +710,19 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 581 |
</div>
|
| 582 |
</div></div>
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
<div class="modal" id="reminders-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 585 |
<h4>Напоминания</h4>
|
| 586 |
<div id="reminders-list" style="max-height: 40vh; overflow-y: auto; margin-bottom: 15px; font-size: 0.9em;"></div>
|
|
@@ -594,7 +736,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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
|
| 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="Название ссылки (необязательно)">
|
|
@@ -686,6 +828,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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,10 +838,20 @@ 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 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
}
|
| 703 |
allItems.forEach(item => {
|
| 704 |
item.addEventListener('pointerdown', e => {
|
|
@@ -751,6 +904,12 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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();
|
|
@@ -851,11 +1010,14 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 851 |
async function openShareModal() {
|
| 852 |
if (selectedItems.size !== 1) return;
|
| 853 |
haptic.impactOccurred('light');
|
| 854 |
-
const
|
|
|
|
|
|
|
| 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',
|
| 859 |
const data = await response.json();
|
| 860 |
listEl.innerHTML = '';
|
| 861 |
if (data.status === 'success' && data.links.length > 0) {
|
|
@@ -875,16 +1037,17 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 875 |
listEl.appendChild(el);
|
| 876 |
});
|
| 877 |
} else {
|
| 878 |
-
listEl.innerHTML = '<p>Публичных ссылок для
|
| 879 |
}
|
| 880 |
}
|
| 881 |
async function createShareLink() {
|
| 882 |
-
const
|
|
|
|
| 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({
|
| 888 |
});
|
| 889 |
const result = await response.json();
|
| 890 |
if (result.status === 'success') {
|
|
@@ -919,6 +1082,120 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 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');
|
|
@@ -956,6 +1233,113 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 956 |
</script></body></html>
|
| 957 |
'''
|
| 958 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
@app.route('/tma_dashboard', methods=['GET', 'POST'])
|
| 960 |
def tma_dashboard():
|
| 961 |
if 'telegram_user_id' not in session:
|
|
@@ -978,8 +1362,10 @@ def tma_dashboard():
|
|
| 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')
|
|
@@ -1033,6 +1419,29 @@ def tma_dashboard():
|
|
| 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():
|
| 1038 |
if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
|
|
@@ -1180,20 +1589,24 @@ def batch_delete_tma():
|
|
| 1180 |
if not node:
|
| 1181 |
errors.append(f"Элемент {item_id} не найден.")
|
| 1182 |
continue
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
| 1185 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1186 |
-
else: errors.append(f'Ошибка удаления папки "{
|
| 1187 |
-
elif
|
| 1188 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1189 |
-
else: errors.append(f'Ошибка удаления "{
|
| 1190 |
-
elif
|
| 1191 |
try:
|
| 1192 |
if node.get('path') and HF_TOKEN_WRITE: api.delete_file(path_in_repo=node['path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 1193 |
except hf_utils.EntryNotFoundError: pass
|
| 1194 |
-
except Exception as e: errors.append(f'Ошибка удаления "{
|
| 1195 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1196 |
-
else: errors.append(f'Ошибка удаления "{
|
|
|
|
| 1197 |
if success_count > 0:
|
| 1198 |
try: save_data(data)
|
| 1199 |
except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'})
|
|
@@ -1304,6 +1717,92 @@ def create_or_update_note_tma():
|
|
| 1304 |
except Exception as e:
|
| 1305 |
return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
|
| 1306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
@app.route('/get_reminders_tma')
|
| 1308 |
def get_reminders_tma():
|
| 1309 |
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
|
|
@@ -1382,38 +1881,35 @@ def create_public_link():
|
|
| 1382 |
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1383 |
|
| 1384 |
payload = request.json
|
| 1385 |
-
|
|
|
|
| 1386 |
name = payload.get('name')
|
| 1387 |
duration_hours = payload.get('duration_hours', 0)
|
| 1388 |
|
| 1389 |
-
|
| 1390 |
-
if not
|
| 1391 |
-
return jsonify({'status': 'error', 'message': '
|
| 1392 |
|
| 1393 |
now = datetime.now(pytz.utc)
|
| 1394 |
-
|
| 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 |
-
'
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 1417 |
return jsonify({'status': 'success', 'url': public_url})
|
| 1418 |
except Exception as e:
|
| 1419 |
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
|
@@ -1430,12 +1926,12 @@ def delete_public_link():
|
|
| 1430 |
if not link_data or link_data.get('user_id') != tma_user_id:
|
| 1431 |
return jsonify({'status': 'error', 'message': 'Ссылка не найдена или нет доступа.'}), 404
|
| 1432 |
|
| 1433 |
-
|
| 1434 |
user_data = data['users'].get(tma_user_id)
|
| 1435 |
if user_data:
|
| 1436 |
-
|
| 1437 |
-
if
|
| 1438 |
-
|
| 1439 |
|
| 1440 |
del data['shared_links'][link_id]
|
| 1441 |
|
|
@@ -1445,23 +1941,26 @@ def delete_public_link():
|
|
| 1445 |
except Exception as e:
|
| 1446 |
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1447 |
|
| 1448 |
-
@app.route('/get_public_links/<
|
| 1449 |
-
def get_public_links(
|
| 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 |
-
|
| 1457 |
-
if not
|
| 1458 |
|
| 1459 |
-
link_ids =
|
| 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
|
|
|
|
|
|
|
|
|
|
| 1465 |
links_details.append(link_data)
|
| 1466 |
|
| 1467 |
return jsonify({'status': 'success', 'links': links_details})
|
|
@@ -1472,7 +1971,7 @@ 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'])
|
|
@@ -1483,7 +1982,7 @@ def shared_folder_view(link_id, subfolder_id=None):
|
|
| 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['
|
| 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':
|
|
@@ -1491,7 +1990,71 @@ def shared_folder_view(link_id, subfolder_id=None):
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1495 |
|
| 1496 |
@app.route('/public_download/<link_id>/<item_id>')
|
| 1497 |
def public_download_via_link(link_id, item_id):
|
|
@@ -1633,12 +2196,12 @@ ADMIN_USER_FILES_HTML = '''
|
|
| 1633 |
<div class="item {{ item.type }}" data-id="{{ item.id }}" data-type="{{ item.type }}">
|
| 1634 |
<div class="item-preview-wrapper"
|
| 1635 |
{% if item.type == 'folder' %} onclick="window.location.href='{{ url_for('admin_user_files', tma_user_id_str=user_id, folder_id=item.id) }}'"
|
| 1636 |
-
{% elif item.type
|
| 1637 |
{% else %} onclick="openModal('{{ hf_file_url_jinja(item.path) if item.file_type not in ['text', 'pdf'] else (url_for('admin_get_text_content', tma_user_id_str=user_id, file_id=item.id) if item.file_type == 'text' else hf_file_url_jinja(item.path, True)) }}', '{{ item.file_type }}', '{{ item.id }}')" {% endif %}>
|
| 1638 |
-
{% if item.type == 'folder' %}
|
| 1639 |
-
|
| 1640 |
-
{% elif item.type == '
|
| 1641 |
-
|
| 1642 |
{% elif item.type == 'file' %}
|
| 1643 |
{% if item.file_type == 'image' %}<img class="item-preview" src="{{ hf_file_url_jinja(item.path) }}" loading="lazy">
|
| 1644 |
{% elif item.file_type == 'video' %}<video class="item-preview" preload="metadata" muted loading="lazy"><source src="{{ hf_file_url_jinja(item.path, True) }}#t=0.5"></video>
|
|
@@ -1649,9 +2212,9 @@ ADMIN_USER_FILES_HTML = '''
|
|
| 1649 |
{% endif %}
|
| 1650 |
</div>
|
| 1651 |
<div class="item-name-info">
|
| 1652 |
-
<p class="item-name">{{ (item.title if item.type
|
| 1653 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 1654 |
-
{% elif item.type
|
| 1655 |
</div>
|
| 1656 |
<div class="item-actions-admin">
|
| 1657 |
<form action="{{ url_for('admin_delete_item', tma_user_id_str=user_id, item_id=item.id) }}" method="post" onsubmit="return confirm('Are you sure you want to delete this?');">
|
|
@@ -1698,11 +2261,16 @@ ADMIN_USER_FILES_HTML = '''
|
|
| 1698 |
downloadBtn.style.display = 'none';
|
| 1699 |
|
| 1700 |
try {
|
| 1701 |
-
if (type === 'note') {
|
| 1702 |
-
const response = await fetch(`{{ url_for('
|
| 1703 |
const data = await response.json();
|
| 1704 |
if(data.status === 'success') {
|
| 1705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1706 |
} else { throw new Error(data.message); }
|
| 1707 |
} else if (type === 'image') {
|
| 1708 |
modalContent.innerHTML = `<img src="${srcOrUrl}">`;
|
|
@@ -1930,16 +2498,15 @@ def admin_get_text_content(tma_user_id_str, file_id):
|
|
| 1930 |
except Exception as e:
|
| 1931 |
return Response(f"Download error: {e}", 502)
|
| 1932 |
|
| 1933 |
-
@app.route('/admhosto/
|
| 1934 |
@admin_browser_login_required
|
| 1935 |
-
def
|
| 1936 |
data = load_data()
|
| 1937 |
user_data = data['users'].get(tma_user_id_str)
|
| 1938 |
if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
| 1939 |
-
node, _ = find_node_by_id(user_data['filesystem'],
|
| 1940 |
-
if not node
|
| 1941 |
-
|
| 1942 |
-
return jsonify({'status': 'success', 'note': node})
|
| 1943 |
|
| 1944 |
@app.route('/admhosto/delete_item/<tma_user_id_str>/<item_id>', methods=['POST'])
|
| 1945 |
@admin_browser_login_required
|
|
@@ -1968,10 +2535,10 @@ def admin_delete_item(tma_user_id_str, item_id):
|
|
| 1968 |
try: save_data(data); flash('File not found on server, removed from DB.')
|
| 1969 |
except Exception: flash('DB save error (file not on server).', 'error')
|
| 1970 |
except Exception as e: flash(f'Deletion error: {e}', 'error')
|
| 1971 |
-
elif node.get('type')
|
| 1972 |
if remove_node(user_data['filesystem'], item_id)[0]:
|
| 1973 |
-
try: save_data(data); flash('
|
| 1974 |
-
except Exception: flash('DB update failed after
|
| 1975 |
elif node.get('type') == 'folder':
|
| 1976 |
if node.get('children'): flash('Folder is not empty.', 'error')
|
| 1977 |
else:
|
|
|
|
| 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; --todolist-color: #20c997; --shoppinglist-color: #fd7e14;
|
| 46 |
}
|
| 47 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 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 |
.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.todolist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--todolist-color); }
|
| 82 |
+
.item.shoppinglist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--shoppinglist-color); }
|
| 83 |
.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
|
| 84 |
.item-info { font-size: 0.75em; color: var(--text-muted); }
|
| 85 |
.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
|
| 86 |
.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
|
| 87 |
.file-grid.list-view .item:hover { transform: translateY(0); }
|
| 88 |
.file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; }
|
| 89 |
+
.file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview, .file-grid.list-view .item.todolist .item-preview, .file-grid.list-view .item.shoppinglist .item-preview { font-size: 1.8em; }
|
| 90 |
.file-grid.list-view .item-name-info { flex-grow: 1; }
|
| 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; }
|
|
|
|
| 106 |
.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; }
|
| 107 |
.fab:active { transform: scale(0.9); }
|
| 108 |
.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; }
|
| 109 |
+
#fab-modal .modal-content { padding: 20px; max-width: 500px; background: var(--card-bg-dark); text-align: center; }
|
| 110 |
+
.fab-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
| 111 |
.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); }
|
| 112 |
.fab-option:hover { background: #333; transform: translateY(-3px); }
|
| 113 |
.fab-option i { font-size: 2em; margin-bottom: 8px; }
|
| 114 |
#fab-option-upload i { color: var(--secondary); }
|
| 115 |
#fab-option-note i { color: var(--note-color); }
|
| 116 |
#fab-option-folder i { color: var(--folder-color); }
|
| 117 |
+
#fab-option-todolist i { color: var(--todolist-color); }
|
| 118 |
+
#fab-option-shoppinglist i { color: var(--shoppinglist-color); }
|
| 119 |
#create-folder-form { display: none; margin-top: 15px; }
|
| 120 |
.shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
|
| 121 |
.shared-link-item:last-child { border-bottom: none; }
|
|
|
|
| 125 |
.shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; }
|
| 126 |
'''
|
| 127 |
|
| 128 |
+
PUBLIC_FOLDER_PAGE_HTML = '''
|
| 129 |
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 130 |
<title>Общая папка: {{ folder.name }}</title>
|
| 131 |
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 180 |
</div></body></html>
|
| 181 |
'''
|
| 182 |
|
| 183 |
+
PUBLIC_SHOPPING_LIST_PAGE_HTML = '''
|
| 184 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 185 |
+
<title>Список покупок: {{ list_data.title }}</title>
|
| 186 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 187 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 188 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 189 |
+
<style>''' + BASE_STYLE + '''
|
| 190 |
+
body { padding-bottom: 30px; }
|
| 191 |
+
.public-header { padding: 15px; text-align: center; border-bottom: 1px solid #333; margin-bottom: 20px; }
|
| 192 |
+
.shopping-list { list-style: none; padding: 0; }
|
| 193 |
+
.shopping-item { display: flex; align-items: center; gap: 15px; background: var(--card-bg-dark); padding: 15px; border-radius: 12px; margin-bottom: 10px; transition: all 0.3s; }
|
| 194 |
+
.shopping-item.purchased { opacity: 0.6; }
|
| 195 |
+
.shopping-item.purchased .item-name { text-decoration: line-through; }
|
| 196 |
+
.shopping-item input[type="checkbox"] { width: 22px; height: 22px; flex-shrink: 0; accent-color: var(--accent); }
|
| 197 |
+
.item-details { flex-grow: 1; }
|
| 198 |
+
.item-name { font-weight: 500; }
|
| 199 |
+
.item-quantity { color: var(--text-muted); font-size: 0.9em; }
|
| 200 |
+
</style></head><body>
|
| 201 |
+
<div class="public-header">
|
| 202 |
+
<h1>Список покупок</h1>
|
| 203 |
+
<h2>{{ list_data.title }}</h2>
|
| 204 |
+
<p style="color: var(--text-muted);">Автор: {{ user.first_name or user.telegram_username }}</p>
|
| 205 |
+
</div>
|
| 206 |
+
<div class="container" style="padding-top: 15px;">
|
| 207 |
+
<ul class="shopping-list" id="shopping-list">
|
| 208 |
+
<div class="loading-spinner"></div>
|
| 209 |
+
</ul>
|
| 210 |
+
</div>
|
| 211 |
+
<script>
|
| 212 |
+
const listId = '{{ link.id }}';
|
| 213 |
+
const listContainer = document.getElementById('shopping-list');
|
| 214 |
+
|
| 215 |
+
function renderList(items) {
|
| 216 |
+
listContainer.innerHTML = '';
|
| 217 |
+
if (!items || items.length === 0) {
|
| 218 |
+
listContainer.innerHTML = '<p>Список пуст.</p>';
|
| 219 |
+
return;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
items.sort((a, b) => a.purchased - b.purchased);
|
| 223 |
+
|
| 224 |
+
items.forEach(item => {
|
| 225 |
+
const li = document.createElement('li');
|
| 226 |
+
li.className = 'shopping-item';
|
| 227 |
+
if (item.purchased) {
|
| 228 |
+
li.classList.add('purchased');
|
| 229 |
+
}
|
| 230 |
+
li.innerHTML = `
|
| 231 |
+
<input type="checkbox" id="item-${item.id}" data-id="${item.id}" ${item.purchased ? 'checked' : ''}>
|
| 232 |
+
<div class="item-details">
|
| 233 |
+
<label for="item-${item.id}" class="item-name">${item.name}</label>
|
| 234 |
+
<div class="item-quantity">Количество: ${item.quantity}</div>
|
| 235 |
+
</div>
|
| 236 |
+
`;
|
| 237 |
+
listContainer.appendChild(li);
|
| 238 |
+
});
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
async function fetchList() {
|
| 242 |
+
try {
|
| 243 |
+
const response = await fetch(`{{ url_for('get_shared_list_data', link_id=link.id) }}`);
|
| 244 |
+
if (!response.ok) throw new Error('Network response was not ok');
|
| 245 |
+
const data = await response.json();
|
| 246 |
+
if (data.status === 'success') {
|
| 247 |
+
renderList(data.items);
|
| 248 |
+
} else {
|
| 249 |
+
listContainer.innerHTML = `<p>Ошибка загрузки списка: ${data.message}</p>`;
|
| 250 |
+
}
|
| 251 |
+
} catch (error) {
|
| 252 |
+
console.error('Fetch error:', error);
|
| 253 |
+
listContainer.innerHTML = '<p>Не удалось обновить список. Проверьте соединение.</p>';
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
async function updateItemStatus(itemId, isPurchased) {
|
| 258 |
+
try {
|
| 259 |
+
await fetch(`{{ url_for('update_shared_item_status', link_id=link.id) }}`, {
|
| 260 |
+
method: 'POST',
|
| 261 |
+
headers: { 'Content-Type': 'application/json' },
|
| 262 |
+
body: JSON.stringify({ item_id: itemId, purchased: isPurchased })
|
| 263 |
+
});
|
| 264 |
+
fetchList();
|
| 265 |
+
} catch (error) {
|
| 266 |
+
console.error('Update error:', error);
|
| 267 |
+
alert('Ошибка обновления статуса товара.');
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
listContainer.addEventListener('change', (event) => {
|
| 272 |
+
if (event.target.type === 'checkbox') {
|
| 273 |
+
const itemId = event.target.dataset.id;
|
| 274 |
+
const isPurchased = event.target.checked;
|
| 275 |
+
updateItemStatus(itemId, isPurchased);
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 280 |
+
fetchList();
|
| 281 |
+
setInterval(fetchList, 5000);
|
| 282 |
+
});
|
| 283 |
+
</script>
|
| 284 |
+
</body></html>
|
| 285 |
+
'''
|
| 286 |
+
|
| 287 |
def find_node_by_id(filesystem, node_id):
|
| 288 |
if not filesystem: return None, None
|
| 289 |
if filesystem.get('id') == node_id:
|
|
|
|
| 346 |
if not node or not isinstance(node, dict):
|
| 347 |
return 0
|
| 348 |
count = 0
|
| 349 |
+
if node.get('type') in ['file', 'note', 'todolist', 'shoppinglist']:
|
| 350 |
count += 1
|
| 351 |
if node.get('type') == 'folder' and 'children' in node:
|
| 352 |
for child in node.get('children', []):
|
|
|
|
| 566 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 567 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 568 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 569 |
+
<style>''' + BASE_STYLE + '''
|
| 570 |
+
.list-editor-items { max-height: 40vh; overflow-y: auto; margin-bottom: 10px; }
|
| 571 |
+
.list-editor-item { display: flex; align-items: center; gap: 10px; padding: 5px; border-radius: 8px; }
|
| 572 |
+
.list-editor-item:hover { background: #333; }
|
| 573 |
+
.list-editor-item input[type="text"] { margin: 0; flex-grow: 1; }
|
| 574 |
+
.list-editor-item input[type="number"] { margin: 0; width: 60px; text-align: center; }
|
| 575 |
+
.list-editor-item .quantity-controls button { padding: 5px 10px; font-size: 1em; background: #444; }
|
| 576 |
+
.list-editor-item .delete-item-btn { color: var(--delete-color); background: none; border: none; font-size: 1.2em; cursor: pointer; }
|
| 577 |
+
.list-editor-add-item-form { display: flex; gap: 10px; margin-top: 10px; }
|
| 578 |
+
</style></head><body>
|
| 579 |
<div class="app-header">
|
| 580 |
<div class="user-info">{{ display_name }}</div>
|
| 581 |
<div class="view-toggle">
|
| 582 |
+
<a href="{{ url_for('tma_archive') }}" style="text-decoration: none;"><button title="Архив"><i class="fa-solid fa-archive"></i></button></a>
|
| 583 |
<button id="reminders-btn" title="Напоминания"><i class="fa-solid fa-bell"></i></button>
|
| 584 |
<button id="grid-view-btn" title="Сетка"><i class="fa fa-th-large"></i></button>
|
| 585 |
<button id="list-view-btn" title="Список"><i class="fa fa-bars"></i></button>
|
|
|
|
| 605 |
onclick="window.Telegram.WebApp.HapticFeedback.impactOccurred('light'); window.location.href='{{ url_for('tma_dashboard', folder_id=item.id) }}'"
|
| 606 |
{% elif item.type == 'note' %}
|
| 607 |
onclick="openNoteModal('{{ item.id }}')"
|
| 608 |
+
{% elif item.type == 'todolist' %}
|
| 609 |
+
onclick="openListModal('todolist', '{{ item.id }}')"
|
| 610 |
+
{% elif item.type == 'shoppinglist' %}
|
| 611 |
+
onclick="openListModal('shoppinglist', '{{ item.id }}')"
|
| 612 |
{% else %}
|
| 613 |
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 }}')"
|
| 614 |
{% endif %}>
|
| 615 |
<div class="item-preview-wrapper">
|
| 616 |
+
{% if item.type == 'folder' %}<div class="item-preview"><i class="fa-solid fa-folder"></i></div>
|
| 617 |
+
{% elif item.type == 'note' %}<div class="item-preview"><i class="fa-solid fa-note-sticky"></i></div>
|
| 618 |
+
{% elif item.type == 'todolist' %}<div class="item-preview"><i class="fa-solid fa-list-check"></i></div>
|
| 619 |
+
{% elif item.type == 'shoppinglist' %}<div class="item-preview"><i class="fa-solid fa-cart-shopping"></i></div>
|
| 620 |
{% elif item.type == 'file' %}
|
| 621 |
{% if item.file_type == 'image' %}<img class="item-preview" src="{{ hf_file_url_jinja(item.path) }}" loading="lazy">
|
| 622 |
{% elif item.file_type == 'video' %}<video class="item-preview" preload="metadata" muted loading="lazy"><source src="{{ hf_file_url_jinja(item.path, True) }}#t=0.5"></video>
|
|
|
|
| 627 |
{% endif %}
|
| 628 |
</div>
|
| 629 |
<div class="item-name-info">
|
| 630 |
+
<p class="item-name">{{ (item.title if item.type in ['note', 'todolist', 'shoppinglist'] else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</p>
|
| 631 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 632 |
+
{% elif item.type in ['note', 'todolist', 'shoppinglist'] %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 633 |
</div>
|
| 634 |
</div>
|
| 635 |
{% endfor %}
|
|
|
|
| 650 |
|
| 651 |
<div id="selection-bar">
|
| 652 |
<span id="selection-count"></span>
|
| 653 |
+
<button id="selection-archive-btn" class="btn" style="background: var(--todolist-color); display:none;" onclick="archiveSelected()"><i class="fa-solid fa-archive"></i></button>
|
| 654 |
<button id="selection-share-btn" class="btn share-btn" onclick="openShareModal()" style="display:none;"><i class="fa-solid fa-share-alt"></i></button>
|
| 655 |
<button id="selection-download-btn" class="btn download-btn" onclick="downloadSingleSelected()" style="display:none;"><i class="fa-solid fa-download"></i></button>
|
| 656 |
<button class="btn" style="background: var(--accent);" onclick="showMoveModal()"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
|
|
|
|
| 679 |
<div class="fab-option" id="fab-option-folder">
|
| 680 |
<i class="fa-solid fa-folder-plus"></i><span>Папку</span>
|
| 681 |
</div>
|
| 682 |
+
<div class="fab-option" id="fab-option-todolist" onclick="openListModal('todolist')">
|
| 683 |
+
<i class="fa-solid fa-list-check"></i><span>Список дел</span>
|
| 684 |
+
</div>
|
| 685 |
+
<div class="fab-option" id="fab-option-shoppinglist" onclick="openListModal('shoppinglist')">
|
| 686 |
+
<i class="fa-solid fa-cart-shopping"></i><span>Покупки</span>
|
| 687 |
+
</div>
|
| 688 |
</div>
|
| 689 |
<form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
|
| 690 |
<input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
|
|
|
|
| 710 |
</div>
|
| 711 |
</div></div>
|
| 712 |
|
| 713 |
+
<div class="modal" id="list-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 714 |
+
<h4 id="list-modal-title">Новый список</h4>
|
| 715 |
+
<input type="hidden" id="list-id-input">
|
| 716 |
+
<input type="hidden" id="list-type-input">
|
| 717 |
+
<input type="text" id="list-title-input" placeholder="Название списка" style="font-size: 1.1em; margin-bottom: 10px;">
|
| 718 |
+
<div id="list-items-container" class="list-editor-items"></div>
|
| 719 |
+
<div id="list-add-item-form-container"></div>
|
| 720 |
+
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
| 721 |
+
<button class="btn" style="background: var(--accent); flex-grow: 1;" onclick="saveList()">Сохранить</button>
|
| 722 |
+
<button class="btn" style="background: #555; flex-grow: 1;" onclick="closeListModal()">Отмена</button>
|
| 723 |
+
</div>
|
| 724 |
+
</div></div>
|
| 725 |
+
|
| 726 |
<div class="modal" id="reminders-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 727 |
<h4>Напоминания</h4>
|
| 728 |
<div id="reminders-list" style="max-height: 40vh; overflow-y: auto; margin-bottom: 15px; font-size: 0.9em;"></div>
|
|
|
|
| 736 |
</div></div>
|
| 737 |
|
| 738 |
<div class="modal" id="share-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 739 |
+
<h4 id="share-modal-title">Поделиться</h4>
|
| 740 |
<div id="existing-links-list" style="max-height: 30vh; overflow-y: auto; margin-bottom: 15px;"></div>
|
| 741 |
<h5 style="margin-top: 15px;">Создать новую ссылку</h5>
|
| 742 |
<input type="text" id="share-link-name" placeholder="Название ссылки (необязательно)">
|
|
|
|
| 828 |
const selectionCount = document.getElementById('selection-count');
|
| 829 |
const selectionDownloadBtn = document.getElementById('selection-download-btn');
|
| 830 |
const selectionShareBtn = document.getElementById('selection-share-btn');
|
| 831 |
+
const selectionArchiveBtn = document.getElementById('selection-archive-btn');
|
| 832 |
const allItems = document.querySelectorAll('.item');
|
| 833 |
function toggleSelectionMode(enable) {
|
| 834 |
selectionMode = enable;
|
|
|
|
| 838 |
}
|
| 839 |
function updateSelectionUI() {
|
| 840 |
selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
|
| 841 |
+
if (selectedItems.size !== 1) {
|
| 842 |
+
selectionDownloadBtn.style.display = 'none';
|
| 843 |
+
selectionShareBtn.style.display = 'none';
|
| 844 |
+
selectionArchiveBtn.style.display = 'none';
|
| 845 |
+
return;
|
| 846 |
+
}
|
| 847 |
const firstSelectedId = selectedItems.values().next().value;
|
| 848 |
const itemElement = document.querySelector(`.item[data-id='${firstSelectedId}']`);
|
| 849 |
+
if (!itemElement) return;
|
| 850 |
+
const itemType = itemElement.dataset.type;
|
| 851 |
+
|
| 852 |
+
selectionDownloadBtn.style.display = (itemType === 'file') ? 'inline-block' : 'none';
|
| 853 |
+
selectionShareBtn.style.display = (itemType === 'folder' || itemType === 'shoppinglist') ? 'inline-block' : 'none';
|
| 854 |
+
selectionArchiveBtn.style.display = (itemType === 'todolist' || itemType === 'shoppinglist') ? 'inline-block' : 'none';
|
| 855 |
}
|
| 856 |
allItems.forEach(item => {
|
| 857 |
item.addEventListener('pointerdown', e => {
|
|
|
|
| 904 |
if (ok) { haptic.impactOccurred('heavy'); performBatchAction('{{ url_for("batch_delete_tma") }}', { item_ids: Array.from(selectedItems) }); }
|
| 905 |
});
|
| 906 |
}
|
| 907 |
+
function archiveSelected() {
|
| 908 |
+
if (selectedItems.size !== 1) return;
|
| 909 |
+
const listId = selectedItems.values().next().value;
|
| 910 |
+
haptic.impactOccurred('medium');
|
| 911 |
+
performBatchAction(`{{ url_for("archive_list_tma", list_id='__ID__') }}`.replace('__ID__', listId), {});
|
| 912 |
+
}
|
| 913 |
async function openNoteModal(noteId = null) {
|
| 914 |
haptic.impactOccurred('light');
|
| 915 |
closeFabModal();
|
|
|
|
| 1010 |
async function openShareModal() {
|
| 1011 |
if (selectedItems.size !== 1) return;
|
| 1012 |
haptic.impactOccurred('light');
|
| 1013 |
+
const itemId = selectedItems.values().next().value;
|
| 1014 |
+
const itemElement = document.querySelector(`.item[data-id='${itemId}']`);
|
| 1015 |
+
const itemType = itemElement.dataset.type;
|
| 1016 |
const listEl = document.getElementById('existing-links-list');
|
| 1017 |
+
document.getElementById('share-modal-title').textContent = itemType === 'folder' ? 'Поделиться папкой' : 'Поделиться списком';
|
| 1018 |
listEl.innerHTML = '<div class="loading-spinner"></div>';
|
| 1019 |
document.getElementById('share-modal').style.display = 'flex';
|
| 1020 |
+
const response = await fetch(`{{ url_for('get_public_links', item_id='ITEM_ID') }}`.replace('ITEM_ID', itemId));
|
| 1021 |
const data = await response.json();
|
| 1022 |
listEl.innerHTML = '';
|
| 1023 |
if (data.status === 'success' && data.links.length > 0) {
|
|
|
|
| 1037 |
listEl.appendChild(el);
|
| 1038 |
});
|
| 1039 |
} else {
|
| 1040 |
+
listEl.innerHTML = '<p>Публичных ссылок для этого элемента нет.</p>';
|
| 1041 |
}
|
| 1042 |
}
|
| 1043 |
async function createShareLink() {
|
| 1044 |
+
const itemId = selectedItems.values().next().value;
|
| 1045 |
+
const itemType = document.querySelector(`.item[data-id='${itemId}']`).dataset.type;
|
| 1046 |
const name = document.getElementById('share-link-name').value;
|
| 1047 |
const duration_hours = document.getElementById('share-link-duration').value;
|
| 1048 |
const response = await fetch('{{ url_for("create_public_link") }}', {
|
| 1049 |
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 1050 |
+
body: JSON.stringify({ item_id: itemId, item_type: itemType, name: name, duration_hours: parseInt(duration_hours) })
|
| 1051 |
});
|
| 1052 |
const result = await response.json();
|
| 1053 |
if (result.status === 'success') {
|
|
|
|
| 1082 |
haptic.notificationOccurred('error');
|
| 1083 |
});
|
| 1084 |
}
|
| 1085 |
+
function setupListModal(type, itemsContainer, addFormContainer) {
|
| 1086 |
+
itemsContainer.innerHTML = '';
|
| 1087 |
+
if (type === 'todolist') {
|
| 1088 |
+
addFormContainer.innerHTML = `
|
| 1089 |
+
<form onsubmit="addTodoItem(event)" class="list-editor-add-item-form">
|
| 1090 |
+
<input type="text" id="add-task-input" placeholder="Новая задача" required>
|
| 1091 |
+
<button type="submit" class="btn" style="padding: 10px 15px;">+</button>
|
| 1092 |
+
</form>`;
|
| 1093 |
+
} else if (type === 'shoppinglist') {
|
| 1094 |
+
addFormContainer.innerHTML = `
|
| 1095 |
+
<form onsubmit="addShoppingItem(event)" class="list-editor-add-item-form">
|
| 1096 |
+
<input type="text" id="add-shopping-name-input" placeholder="Название товара" required style="flex-grow:1;">
|
| 1097 |
+
<input type="number" id="add-shopping-qty-input" value="1" min="1" style="width: 70px;">
|
| 1098 |
+
<button type="submit" class="btn" style="padding: 10px 15px;">+</button>
|
| 1099 |
+
</form>`;
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
function renderTodoItem(item) {
|
| 1103 |
+
return `<div class="list-editor-item" data-id="${item.id}">
|
| 1104 |
+
<input type="checkbox" ${item.completed ? 'checked' : ''}>
|
| 1105 |
+
<input type="text" value="${item.text.replace(/"/g, '"')}">
|
| 1106 |
+
<button class="delete-item-btn" onclick="this.parentElement.remove()">×</button>
|
| 1107 |
+
</div>`;
|
| 1108 |
+
}
|
| 1109 |
+
function renderShoppingItem(item) {
|
| 1110 |
+
return `<div class="list-editor-item" data-id="${item.id}">
|
| 1111 |
+
<input type="text" value="${item.name.replace(/"/g, '"')}" class="shopping-item-name">
|
| 1112 |
+
<div class="quantity-controls" style="display:flex; align-items:center;">
|
| 1113 |
+
<button type="button" onclick="this.nextElementSibling.stepDown()">-</button>
|
| 1114 |
+
<input type="number" value="${item.quantity}" min="1" class="shopping-item-qty">
|
| 1115 |
+
<button type="button" onclick="this.previousElementSibling.stepUp()">+</button>
|
| 1116 |
+
</div>
|
| 1117 |
+
<button class="delete-item-btn" onclick="this.parentElement.remove()">×</button>
|
| 1118 |
+
</div>`;
|
| 1119 |
+
}
|
| 1120 |
+
function addTodoItem(event) {
|
| 1121 |
+
event.preventDefault();
|
| 1122 |
+
const input = document.getElementById('add-task-input');
|
| 1123 |
+
if (!input.value.trim()) return;
|
| 1124 |
+
const newItem = { id: 'new_' + Date.now(), text: input.value.trim(), completed: false };
|
| 1125 |
+
document.getElementById('list-items-container').insertAdjacentHTML('beforeend', renderTodoItem(newItem));
|
| 1126 |
+
input.value = '';
|
| 1127 |
+
}
|
| 1128 |
+
function addShoppingItem(event) {
|
| 1129 |
+
event.preventDefault();
|
| 1130 |
+
const nameInput = document.getElementById('add-shopping-name-input');
|
| 1131 |
+
const qtyInput = document.getElementById('add-shopping-qty-input');
|
| 1132 |
+
if (!nameInput.value.trim()) return;
|
| 1133 |
+
const newItem = { id: 'new_' + Date.now(), name: nameInput.value.trim(), quantity: parseInt(qtyInput.value) || 1, purchased: false };
|
| 1134 |
+
document.getElementById('list-items-container').insertAdjacentHTML('beforeend', renderShoppingItem(newItem));
|
| 1135 |
+
nameInput.value = '';
|
| 1136 |
+
qtyInput.value = '1';
|
| 1137 |
+
}
|
| 1138 |
+
async function openListModal(type, listId = null) {
|
| 1139 |
+
haptic.impactOccurred('light');
|
| 1140 |
+
closeFabModal();
|
| 1141 |
+
const modal = document.getElementById('list-modal');
|
| 1142 |
+
const titleEl = document.getElementById('list-modal-title');
|
| 1143 |
+
const idInput = document.getElementById('list-id-input');
|
| 1144 |
+
const typeInput = document.getElementById('list-type-input');
|
| 1145 |
+
const titleInput = document.getElementById('list-title-input');
|
| 1146 |
+
const itemsContainer = document.getElementById('list-items-container');
|
| 1147 |
+
const addFormContainer = document.getElementById('list-add-item-form-container');
|
| 1148 |
+
|
| 1149 |
+
typeInput.value = type;
|
| 1150 |
+
setupListModal(type, itemsContainer, addFormContainer);
|
| 1151 |
+
|
| 1152 |
+
if (listId) {
|
| 1153 |
+
titleEl.textContent = type === 'todolist' ? 'Редактировать список дел' : 'Редактировать список покупок';
|
| 1154 |
+
const response = await fetch(`{{ url_for('get_list_tma', list_id='__ID__') }}`.replace('__ID__', listId));
|
| 1155 |
+
const data = await response.json();
|
| 1156 |
+
if (data.status === 'success') {
|
| 1157 |
+
idInput.value = data.list.id;
|
| 1158 |
+
titleInput.value = data.list.title;
|
| 1159 |
+
itemsContainer.innerHTML = '';
|
| 1160 |
+
if (type === 'todolist') data.list.tasks.forEach(item => itemsContainer.insertAdjacentHTML('beforeend', renderTodoItem(item)));
|
| 1161 |
+
if (type === 'shoppinglist') data.list.items.forEach(item => itemsContainer.insertAdjacentHTML('beforeend', renderShoppingItem(item)));
|
| 1162 |
+
} else { Telegram.WebApp.showAlert('Ошибка загрузки списка.'); return; }
|
| 1163 |
+
} else {
|
| 1164 |
+
titleEl.textContent = type === 'todolist' ? 'Новый список дел' : 'Новый список покупок';
|
| 1165 |
+
idInput.value = ''; titleInput.value = ''; itemsContainer.innerHTML = '';
|
| 1166 |
+
}
|
| 1167 |
+
modal.style.display = 'flex';
|
| 1168 |
+
}
|
| 1169 |
+
function closeListModal() { document.getElementById('list-modal').style.display = 'none'; }
|
| 1170 |
+
async function saveList() {
|
| 1171 |
+
const id = document.getElementById('list-id-input').value;
|
| 1172 |
+
const type = document.getElementById('list-type-input').value;
|
| 1173 |
+
const title = document.getElementById('list-title-input').value;
|
| 1174 |
+
if (!title.trim()) { Telegram.WebApp.showAlert('Название не может быть пустым.'); return; }
|
| 1175 |
+
|
| 1176 |
+
let payload = { list_id: id, type: type, title: title, parent_folder_id: '{{ current_folder_id }}' };
|
| 1177 |
+
|
| 1178 |
+
if (type === 'todolist') {
|
| 1179 |
+
payload.tasks = Array.from(document.querySelectorAll('#list-items-container .list-editor-item')).map(el => ({
|
| 1180 |
+
id: el.dataset.id.startsWith('new_') ? null : el.dataset.id,
|
| 1181 |
+
text: el.querySelector('input[type="text"]').value,
|
| 1182 |
+
completed: el.querySelector('input[type="checkbox"]').checked
|
| 1183 |
+
}));
|
| 1184 |
+
} else if (type === 'shoppinglist') {
|
| 1185 |
+
payload.items = Array.from(document.querySelectorAll('#list-items-container .list-editor-item')).map(el => ({
|
| 1186 |
+
id: el.dataset.id.startsWith('new_') ? null : el.dataset.id,
|
| 1187 |
+
name: el.querySelector('.shopping-item-name').value,
|
| 1188 |
+
quantity: parseInt(el.querySelector('.shopping-item-qty').value) || 1
|
| 1189 |
+
}));
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
const response = await fetch('{{ url_for("create_or_update_list_tma") }}', {
|
| 1193 |
+
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
|
| 1194 |
+
});
|
| 1195 |
+
const result = await response.json();
|
| 1196 |
+
if (result.status === 'success') { haptic.notificationOccurred('success'); window.location.reload(); }
|
| 1197 |
+
else { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert(result.message || 'Ошибка сохранения.'); }
|
| 1198 |
+
}
|
| 1199 |
document.getElementById('reminders-btn').addEventListener('click', openRemindersModal);
|
| 1200 |
const gridViewBtn = document.getElementById('grid-view-btn');
|
| 1201 |
const listViewBtn = document.getElementById('list-view-btn');
|
|
|
|
| 1233 |
</script></body></html>
|
| 1234 |
'''
|
| 1235 |
|
| 1236 |
+
TMA_ARCHIVE_PAGE_HTML = '''
|
| 1237 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 1238 |
+
<title>Архив - Zeus Cloud</title>
|
| 1239 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 1240 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 1241 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 1242 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 1243 |
+
<style>''' + BASE_STYLE + '''</style></head><body>
|
| 1244 |
+
<div class="app-header">
|
| 1245 |
+
<div class="user-info">{{ display_name }}</div>
|
| 1246 |
+
<div class="view-toggle">
|
| 1247 |
+
<a href="{{ url_for('tma_dashboard') }}" style="text-decoration: none;"><button title="Домой"><i class="fa-solid fa-home"></i></button></a>
|
| 1248 |
+
</div>
|
| 1249 |
+
</div>
|
| 1250 |
+
<div class="container" id="main-container">
|
| 1251 |
+
<h2>Архив</h2>
|
| 1252 |
+
<div class="file-grid list-view" id="file-container">
|
| 1253 |
+
{% for item in items %}
|
| 1254 |
+
<div class="item {{ item.type }}" data-id="{{ item.id }}" data-type="{{ item.type }}">
|
| 1255 |
+
<div class="item-preview-wrapper">
|
| 1256 |
+
{% if item.type == 'todolist' %}<div class="item-preview"><i class="fa-solid fa-list-check"></i></div>
|
| 1257 |
+
{% elif item.type == 'shoppinglist' %}<div class="item-preview"><i class="fa-solid fa-cart-shopping"></i></div>
|
| 1258 |
+
{% endif %}
|
| 1259 |
+
</div>
|
| 1260 |
+
<div class="item-name-info" style="flex-grow: 1;">
|
| 1261 |
+
<p class="item-name">{{ item.title }}</p>
|
| 1262 |
+
<p class="item-info">Заархивировано: {{ item.modified_date }}</p>
|
| 1263 |
+
<p class="item-info">Путь: {{ item.path_string }}</p>
|
| 1264 |
+
</div>
|
| 1265 |
+
<button class="btn" onclick="unarchiveItem('{{ item.id }}')" title="Разархивировать" style="background:var(--secondary); padding: 8px 12px; margin-right: 5px;"><i class="fa-solid fa-box-open"></i></button>
|
| 1266 |
+
<button class="btn delete-btn" onclick="deleteItem('{{ item.id }}')" title="Удалить" style="padding: 8px 12px;"><i class="fa-solid fa-trash"></i></button>
|
| 1267 |
+
</div>
|
| 1268 |
+
{% endfor %}
|
| 1269 |
+
{% if not items %}<p>Архив пуст.</p>{% endif %}
|
| 1270 |
+
</div>
|
| 1271 |
+
</div>
|
| 1272 |
+
<script>
|
| 1273 |
+
window.Telegram.WebApp.ready();
|
| 1274 |
+
window.Telegram.WebApp.expand();
|
| 1275 |
+
const haptic = window.Telegram.WebApp.HapticFeedback;
|
| 1276 |
+
|
| 1277 |
+
async function performAction(url, confirmMessage) {
|
| 1278 |
+
Telegram.WebApp.showConfirm(confirmMessage, async (ok) => {
|
| 1279 |
+
if (ok) {
|
| 1280 |
+
haptic.impactOccurred('heavy');
|
| 1281 |
+
try {
|
| 1282 |
+
const response = await fetch(url, { method: 'POST' });
|
| 1283 |
+
const result = await response.json();
|
| 1284 |
+
if (result.status === 'success') {
|
| 1285 |
+
haptic.notificationOccurred('success');
|
| 1286 |
+
window.location.reload();
|
| 1287 |
+
} else {
|
| 1288 |
+
haptic.notificationOccurred('error');
|
| 1289 |
+
Telegram.WebApp.showAlert(result.message || 'Произошла ошибка.');
|
| 1290 |
+
}
|
| 1291 |
+
} catch (error) {
|
| 1292 |
+
haptic.notificationOccurred('error');
|
| 1293 |
+
Telegram.WebApp.showAlert('Сетевая ошибка.');
|
| 1294 |
+
}
|
| 1295 |
+
}
|
| 1296 |
+
});
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
function unarchiveItem(itemId) {
|
| 1300 |
+
const url = `{{ url_for("archive_list_tma", list_id='__ID__') }}`.replace('__ID__', itemId) + '?unarchive=true';
|
| 1301 |
+
performAction(url, 'Разархивировать этот список?');
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
function deleteItem(itemId) {
|
| 1305 |
+
const url = `{{ url_for("batch_delete_tma") }}`;
|
| 1306 |
+
Telegram.WebApp.showConfirm('Вы уверены, что хотите удалить этот список навсегда?', async (ok) => {
|
| 1307 |
+
if (ok) {
|
| 1308 |
+
haptic.impactOccurred('heavy');
|
| 1309 |
+
try {
|
| 1310 |
+
const response = await fetch(url, {
|
| 1311 |
+
method: 'POST',
|
| 1312 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1313 |
+
body: JSON.stringify({ item_ids: [itemId] })
|
| 1314 |
+
});
|
| 1315 |
+
const result = await response.json();
|
| 1316 |
+
if (result.status === 'success') {
|
| 1317 |
+
haptic.notificationOccurred('success');
|
| 1318 |
+
window.location.reload();
|
| 1319 |
+
} else {
|
| 1320 |
+
haptic.notificationOccurred('error');
|
| 1321 |
+
Telegram.WebApp.showAlert(result.message || 'Произошла ошибка.');
|
| 1322 |
+
}
|
| 1323 |
+
} catch (error) {
|
| 1324 |
+
haptic.notificationOccurred('error');
|
| 1325 |
+
Telegram.WebApp.showAlert('Сетевая ошибка.');
|
| 1326 |
+
}
|
| 1327 |
+
}
|
| 1328 |
+
});
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1332 |
+
let backButton = window.Telegram.WebApp.BackButton;
|
| 1333 |
+
backButton.show();
|
| 1334 |
+
backButton.onClick(() => {
|
| 1335 |
+
haptic.impactOccurred('light');
|
| 1336 |
+
window.location.href = `{{ url_for('tma_dashboard') }}`;
|
| 1337 |
+
});
|
| 1338 |
+
});
|
| 1339 |
+
</script>
|
| 1340 |
+
</body></html>
|
| 1341 |
+
'''
|
| 1342 |
+
|
| 1343 |
@app.route('/tma_dashboard', methods=['GET', 'POST'])
|
| 1344 |
def tma_dashboard():
|
| 1345 |
if 'telegram_user_id' not in session:
|
|
|
|
| 1362 |
current_folder, parent_folder = find_node_by_id(user_data['filesystem'], 'root')
|
| 1363 |
|
| 1364 |
parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root'
|
| 1365 |
+
|
| 1366 |
+
items_in_folder = [item for item in current_folder.get('children', []) if not item.get('archived')]
|
| 1367 |
+
items_in_folder = sorted(items_in_folder, key=lambda x: (x['type'] not in ['folder', 'note'], x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 1368 |
|
|
|
|
| 1369 |
if request.method == 'POST':
|
| 1370 |
if not HF_TOKEN_WRITE:
|
| 1371 |
flash('Загрузка невозможна: токен для записи не настроен.', 'error')
|
|
|
|
| 1419 |
|
| 1420 |
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)
|
| 1421 |
|
| 1422 |
+
@app.route('/tma_archive')
|
| 1423 |
+
def tma_archive():
|
| 1424 |
+
if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
|
| 1425 |
+
tma_user_id = session['telegram_user_id']
|
| 1426 |
+
display_name = session.get('telegram_display_name', 'Пользователь')
|
| 1427 |
+
data = load_data()
|
| 1428 |
+
user_data = data['users'].get(tma_user_id)
|
| 1429 |
+
if not user_data: return redirect(url_for('tma_entry_page'))
|
| 1430 |
+
|
| 1431 |
+
archived_items = []
|
| 1432 |
+
def find_archived(node):
|
| 1433 |
+
if node.get('type') == 'folder':
|
| 1434 |
+
for child in node.get('children', []):
|
| 1435 |
+
if child.get('archived'):
|
| 1436 |
+
child['path_string'] = get_node_path_string(user_data['filesystem'], node.get('id'))
|
| 1437 |
+
archived_items.append(child)
|
| 1438 |
+
if child.get('type') == 'folder':
|
| 1439 |
+
find_archived(child)
|
| 1440 |
+
find_archived(user_data.get('filesystem'))
|
| 1441 |
+
|
| 1442 |
+
sorted_items = sorted(archived_items, key=lambda x: x.get('modified_date', ''), reverse=True)
|
| 1443 |
+
return render_template_string(TMA_ARCHIVE_PAGE_HTML, display_name=display_name, items=sorted_items)
|
| 1444 |
+
|
| 1445 |
@app.route('/create_folder_tma', methods=['POST'])
|
| 1446 |
def create_folder_tma():
|
| 1447 |
if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
|
|
|
|
| 1589 |
if not node:
|
| 1590 |
errors.append(f"Элемент {item_id} не найден.")
|
| 1591 |
continue
|
| 1592 |
+
item_type = node.get('type')
|
| 1593 |
+
item_name = node.get('name') or node.get('title') or node.get('original_filename')
|
| 1594 |
+
|
| 1595 |
+
if item_type == 'folder':
|
| 1596 |
+
if node.get('children'): errors.append(f'Папка "{item_name}" не пуста.'); continue
|
| 1597 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1598 |
+
else: errors.append(f'Ошибка удаления папки "{item_name}".')
|
| 1599 |
+
elif item_type in ['note', 'todolist', 'shoppinglist']:
|
| 1600 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1601 |
+
else: errors.append(f'Ошибка удаления "{item_name}".')
|
| 1602 |
+
elif item_type == 'file':
|
| 1603 |
try:
|
| 1604 |
if node.get('path') and HF_TOKEN_WRITE: api.delete_file(path_in_repo=node['path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 1605 |
except hf_utils.EntryNotFoundError: pass
|
| 1606 |
+
except Exception as e: errors.append(f'Ошибка удаления "{item_name}" с сервера: {e}'); continue
|
| 1607 |
if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1
|
| 1608 |
+
else: errors.append(f'Ошибка удаления "{item_name}" из базы.')
|
| 1609 |
+
|
| 1610 |
if success_count > 0:
|
| 1611 |
try: save_data(data)
|
| 1612 |
except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'})
|
|
|
|
| 1717 |
except Exception as e:
|
| 1718 |
return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
|
| 1719 |
|
| 1720 |
+
@app.route('/get_list_tma/<list_id>')
|
| 1721 |
+
def get_list_tma(list_id):
|
| 1722 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
|
| 1723 |
+
list_node = get_item_node_for_user(list_id)
|
| 1724 |
+
if not list_node or list_node.get('type') not in ['todolist', 'shoppinglist']:
|
| 1725 |
+
return jsonify({'status': 'error', 'message': 'List not found'}), 404
|
| 1726 |
+
return jsonify({'status': 'success', 'list': list_node})
|
| 1727 |
+
|
| 1728 |
+
@app.route('/create_or_update_list_tma', methods=['POST'])
|
| 1729 |
+
def create_or_update_list_tma():
|
| 1730 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
|
| 1731 |
+
tma_user_id = session['telegram_user_id']
|
| 1732 |
+
data = load_data()
|
| 1733 |
+
user_data = data['users'].get(tma_user_id)
|
| 1734 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
| 1735 |
+
|
| 1736 |
+
payload = request.json
|
| 1737 |
+
list_id = payload.get('list_id')
|
| 1738 |
+
list_type = payload.get('type')
|
| 1739 |
+
title = payload.get('title', '').strip()
|
| 1740 |
+
parent_folder_id = payload.get('parent_folder_id', 'root')
|
| 1741 |
+
now_str = datetime.now().strftime('%Y-%m-%d %H:%M')
|
| 1742 |
+
|
| 1743 |
+
if not title or list_type not in ['todolist', 'shoppinglist']:
|
| 1744 |
+
return jsonify({'status': 'error', 'message': 'Invalid data provided.'}), 400
|
| 1745 |
+
|
| 1746 |
+
if list_id:
|
| 1747 |
+
node, _ = find_node_by_id(user_data['filesystem'], list_id)
|
| 1748 |
+
if not node or node.get('type') != list_type:
|
| 1749 |
+
return jsonify({'status': 'error', 'message': 'List not found'}), 404
|
| 1750 |
+
node['title'] = title
|
| 1751 |
+
node['modified_date'] = now_str
|
| 1752 |
+
if list_type == 'todolist':
|
| 1753 |
+
node['tasks'] = payload.get('tasks', [])
|
| 1754 |
+
for task in node['tasks']:
|
| 1755 |
+
if not task.get('id'): task['id'] = uuid.uuid4().hex
|
| 1756 |
+
elif list_type == 'shoppinglist':
|
| 1757 |
+
existing_items = {item['id']: item for item in node.get('items', [])}
|
| 1758 |
+
new_items_data = payload.get('items', [])
|
| 1759 |
+
updated_items = []
|
| 1760 |
+
for item_data in new_items_data:
|
| 1761 |
+
item_id = item_data.get('id')
|
| 1762 |
+
if item_id and item_id in existing_items:
|
| 1763 |
+
existing_items[item_id].update(item_data)
|
| 1764 |
+
updated_items.append(existing_items[item_id])
|
| 1765 |
+
else:
|
| 1766 |
+
item_data['id'] = uuid.uuid4().hex
|
| 1767 |
+
item_data['purchased'] = False
|
| 1768 |
+
updated_items.append(item_data)
|
| 1769 |
+
node['items'] = updated_items
|
| 1770 |
+
|
| 1771 |
+
else:
|
| 1772 |
+
new_list_id = uuid.uuid4().hex
|
| 1773 |
+
list_data = {
|
| 1774 |
+
'type': list_type, 'id': new_list_id, 'title': title, 'archived': False,
|
| 1775 |
+
'created_date': now_str, 'modified_date': now_str
|
| 1776 |
+
}
|
| 1777 |
+
if list_type == 'todolist':
|
| 1778 |
+
list_data['tasks'] = payload.get('tasks', [])
|
| 1779 |
+
for task in list_data['tasks']: task['id'] = uuid.uuid4().hex
|
| 1780 |
+
elif list_type == 'shoppinglist':
|
| 1781 |
+
list_data['items'] = payload.get('items', [])
|
| 1782 |
+
for item in list_data['items']:
|
| 1783 |
+
item['id'] = uuid.uuid4().hex
|
| 1784 |
+
item['purchased'] = False
|
| 1785 |
+
|
| 1786 |
+
if not add_node(user_data['filesystem'], parent_folder_id, list_data):
|
| 1787 |
+
return jsonify({'status': 'error', 'message': 'Parent folder not found'}), 404
|
| 1788 |
+
|
| 1789 |
+
try: save_data(data); return jsonify({'status': 'success', 'message': 'List saved.'})
|
| 1790 |
+
except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
|
| 1791 |
+
|
| 1792 |
+
@app.route('/archive_list_tma/<list_id>', methods=['POST'])
|
| 1793 |
+
def archive_list_tma(list_id):
|
| 1794 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
|
| 1795 |
+
list_node = get_item_node_for_user(list_id)
|
| 1796 |
+
if not list_node or list_node.get('type') not in ['todolist', 'shoppinglist']:
|
| 1797 |
+
return jsonify({'status': 'error', 'message': 'List not found'}), 404
|
| 1798 |
+
|
| 1799 |
+
unarchive = request.args.get('unarchive', 'false').lower() == 'true'
|
| 1800 |
+
list_node['archived'] = not unarchive
|
| 1801 |
+
list_node['modified_date'] = datetime.now().strftime('%Y-%m-%d %H:%M')
|
| 1802 |
+
|
| 1803 |
+
try: save_data(load_data()); return jsonify({'status': 'success'})
|
| 1804 |
+
except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500
|
| 1805 |
+
|
| 1806 |
@app.route('/get_reminders_tma')
|
| 1807 |
def get_reminders_tma():
|
| 1808 |
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
|
|
|
|
| 1881 |
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1882 |
|
| 1883 |
payload = request.json
|
| 1884 |
+
item_id = payload.get('item_id')
|
| 1885 |
+
item_type = payload.get('item_type')
|
| 1886 |
name = payload.get('name')
|
| 1887 |
duration_hours = payload.get('duration_hours', 0)
|
| 1888 |
|
| 1889 |
+
item_node, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 1890 |
+
if not item_node or item_node.get('type') != item_type or item_type not in ['folder', 'shoppinglist']:
|
| 1891 |
+
return jsonify({'status': 'error', 'message': 'Элемент не найден или не может быть опубликован.'}), 404
|
| 1892 |
|
| 1893 |
now = datetime.now(pytz.utc)
|
| 1894 |
+
expires_at_iso = None
|
| 1895 |
if duration_hours > 0:
|
| 1896 |
expires_at = now + timedelta(hours=duration_hours)
|
| 1897 |
expires_at_iso = expires_at.isoformat()
|
|
|
|
|
|
|
| 1898 |
|
| 1899 |
link_id = uuid.uuid4().hex
|
| 1900 |
link_data = {
|
| 1901 |
+
'id': link_id, 'user_id': tma_user_id, 'item_id': item_id, 'item_type': item_type,
|
| 1902 |
+
'name': name, 'created_at': now.isoformat(), 'expires_at': expires_at_iso
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1903 |
}
|
| 1904 |
data['shared_links'][link_id] = link_data
|
| 1905 |
+
item_node.setdefault('public_links', []).append(link_id)
|
|
|
|
| 1906 |
|
| 1907 |
try:
|
| 1908 |
save_data(data)
|
| 1909 |
+
if item_type == 'folder':
|
| 1910 |
+
public_url = url_for('shared_folder_view', link_id=link_id, _external=True)
|
| 1911 |
+
else: # shoppinglist
|
| 1912 |
+
public_url = url_for('shared_list_view', link_id=link_id, _external=True)
|
| 1913 |
return jsonify({'status': 'success', 'url': public_url})
|
| 1914 |
except Exception as e:
|
| 1915 |
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
|
|
|
| 1926 |
if not link_data or link_data.get('user_id') != tma_user_id:
|
| 1927 |
return jsonify({'status': 'error', 'message': 'Ссылка не найдена или нет доступа.'}), 404
|
| 1928 |
|
| 1929 |
+
item_id = link_data.get('item_id')
|
| 1930 |
user_data = data['users'].get(tma_user_id)
|
| 1931 |
if user_data:
|
| 1932 |
+
item, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 1933 |
+
if item and 'public_links' in item:
|
| 1934 |
+
item['public_links'] = [l for l in item['public_links'] if l != link_id]
|
| 1935 |
|
| 1936 |
del data['shared_links'][link_id]
|
| 1937 |
|
|
|
|
| 1941 |
except Exception as e:
|
| 1942 |
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1943 |
|
| 1944 |
+
@app.route('/get_public_links/<item_id>')
|
| 1945 |
+
def get_public_links(item_id):
|
| 1946 |
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1947 |
tma_user_id = session['telegram_user_id']
|
| 1948 |
data = load_data()
|
| 1949 |
user_data = data['users'].get(tma_user_id)
|
| 1950 |
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1951 |
|
| 1952 |
+
item, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 1953 |
+
if not item: return jsonify({'status': 'error', 'message': 'Элемент не найден.'}), 404
|
| 1954 |
|
| 1955 |
+
link_ids = item.get('public_links', [])
|
| 1956 |
links_details = []
|
| 1957 |
for link_id in link_ids:
|
| 1958 |
link_data = data['shared_links'].get(link_id)
|
| 1959 |
if link_data:
|
| 1960 |
+
if link_data.get('item_type') == 'folder':
|
| 1961 |
+
link_data['url'] = url_for('shared_folder_view', link_id=link_id, _external=True)
|
| 1962 |
+
else:
|
| 1963 |
+
link_data['url'] = url_for('shared_list_view', link_id=link_id, _external=True)
|
| 1964 |
links_details.append(link_data)
|
| 1965 |
|
| 1966 |
return jsonify({'status': 'success', 'links': links_details})
|
|
|
|
| 1971 |
data = load_data()
|
| 1972 |
link_data = data['shared_links'].get(link_id)
|
| 1973 |
|
| 1974 |
+
if not link_data or link_data.get('item_type') != 'folder': return "Ссылка недействительна.", 404
|
| 1975 |
|
| 1976 |
if link_data.get('expires_at'):
|
| 1977 |
expires_at = datetime.fromisoformat(link_data['expires_at'])
|
|
|
|
| 1982 |
user_data = data['users'].get(user_id)
|
| 1983 |
if not user_data: return "Владелец не найден.", 404
|
| 1984 |
|
| 1985 |
+
folder_id_to_show = subfolder_id if subfolder_id else link_data['item_id']
|
| 1986 |
folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id_to_show)
|
| 1987 |
|
| 1988 |
if not folder_node or folder_node.get('type') != 'folder':
|
|
|
|
| 1990 |
|
| 1991 |
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()))
|
| 1992 |
|
| 1993 |
+
return render_template_string(PUBLIC_FOLDER_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 ''}")
|
| 1994 |
+
|
| 1995 |
+
@app.route('/shared_list/<link_id>')
|
| 1996 |
+
def shared_list_view(link_id):
|
| 1997 |
+
data = load_data()
|
| 1998 |
+
link_data = data['shared_links'].get(link_id)
|
| 1999 |
+
if not link_data or link_data.get('item_type') != 'shoppinglist': return "Ссылка недействительна.", 404
|
| 2000 |
+
if link_data.get('expires_at'):
|
| 2001 |
+
expires_at = datetime.fromisoformat(link_data['expires_at'])
|
| 2002 |
+
if datetime.now(pytz.utc) > expires_at: return "Срок действия ссылки истек.", 410
|
| 2003 |
+
|
| 2004 |
+
user_id = link_data['user_id']
|
| 2005 |
+
user_data = data['users'].get(user_id)
|
| 2006 |
+
if not user_data: return "Владелец не найден.", 404
|
| 2007 |
+
|
| 2008 |
+
list_node, _ = find_node_by_id(user_data['filesystem'], link_data['item_id'])
|
| 2009 |
+
if not list_node or list_node.get('type') != 'shoppinglist': return "Список не найден.", 404
|
| 2010 |
+
|
| 2011 |
+
return render_template_string(PUBLIC_SHOPPING_LIST_PAGE_HTML, list_data=list_node, user=user_data, link=link_data)
|
| 2012 |
+
|
| 2013 |
+
@app.route('/get_shared_list_data/<link_id>')
|
| 2014 |
+
def get_shared_list_data(link_id):
|
| 2015 |
+
data = load_data()
|
| 2016 |
+
link_data = data['shared_links'].get(link_id)
|
| 2017 |
+
if not link_data or link_data.get('item_type') != 'shoppinglist': return jsonify({'status':'error', 'message':'Ссылка недействительна'}), 404
|
| 2018 |
+
user_id = link_data['user_id']
|
| 2019 |
+
user_data = data['users'].get(user_id)
|
| 2020 |
+
if not user_data: return jsonify({'status':'error', 'message':'Владелец не найден'}), 404
|
| 2021 |
+
list_node, _ = find_node_by_id(user_data['filesystem'], link_data['item_id'])
|
| 2022 |
+
if not list_node: return jsonify({'status':'error', 'message':'Список не найден'}), 404
|
| 2023 |
+
|
| 2024 |
+
return jsonify({'status': 'success', 'items': list_node.get('items', [])})
|
| 2025 |
+
|
| 2026 |
+
@app.route('/update_shared_item_status/<link_id>', methods=['POST'])
|
| 2027 |
+
def update_shared_item_status(link_id):
|
| 2028 |
+
data = load_data()
|
| 2029 |
+
link_data = data['shared_links'].get(link_id)
|
| 2030 |
+
if not link_data or link_data.get('item_type') != 'shoppinglist': return jsonify({'status': 'error', 'message': 'Ссылка недействительна'}), 404
|
| 2031 |
+
|
| 2032 |
+
user_id = link_data['user_id']
|
| 2033 |
+
user_data = data['users'].get(user_id)
|
| 2034 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Владелец не найден'}), 404
|
| 2035 |
+
|
| 2036 |
+
list_node, _ = find_node_by_id(user_data['filesystem'], link_data['item_id'])
|
| 2037 |
+
if not list_node: return jsonify({'status': 'error', 'message': 'Список не найден'}), 404
|
| 2038 |
+
|
| 2039 |
+
payload = request.json
|
| 2040 |
+
item_id_to_update = payload.get('item_id')
|
| 2041 |
+
purchased_status = payload.get('purchased')
|
| 2042 |
+
|
| 2043 |
+
item_found = False
|
| 2044 |
+
for item in list_node.get('items', []):
|
| 2045 |
+
if item.get('id') == item_id_to_update:
|
| 2046 |
+
item['purchased'] = purchased_status
|
| 2047 |
+
item_found = True
|
| 2048 |
+
break
|
| 2049 |
+
|
| 2050 |
+
if item_found:
|
| 2051 |
+
try:
|
| 2052 |
+
save_data(data)
|
| 2053 |
+
return jsonify({'status': 'success'})
|
| 2054 |
+
except Exception as e:
|
| 2055 |
+
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 2056 |
+
else:
|
| 2057 |
+
return jsonify({'status': 'error', 'message': 'Элемент не найден в списке'}), 404
|
| 2058 |
|
| 2059 |
@app.route('/public_download/<link_id>/<item_id>')
|
| 2060 |
def public_download_via_link(link_id, item_id):
|
|
|
|
| 2196 |
<div class="item {{ item.type }}" data-id="{{ item.id }}" data-type="{{ item.type }}">
|
| 2197 |
<div class="item-preview-wrapper"
|
| 2198 |
{% if item.type == 'folder' %} onclick="window.location.href='{{ url_for('admin_user_files', tma_user_id_str=user_id, folder_id=item.id) }}'"
|
| 2199 |
+
{% elif item.type in ['note', 'todolist', 'shoppinglist'] %} onclick="openModal(null, '{{item.type}}', '{{ item.id }}')"
|
| 2200 |
{% else %} onclick="openModal('{{ hf_file_url_jinja(item.path) if item.file_type not in ['text', 'pdf'] else (url_for('admin_get_text_content', tma_user_id_str=user_id, file_id=item.id) if item.file_type == 'text' else hf_file_url_jinja(item.path, True)) }}', '{{ item.file_type }}', '{{ item.id }}')" {% endif %}>
|
| 2201 |
+
{% if item.type == 'folder' %}<div class="item-preview"><i class="fa-solid fa-folder"></i></div>
|
| 2202 |
+
{% elif item.type == 'note' %}<div class="item-preview"><i class="fa-solid fa-note-sticky"></i></div>
|
| 2203 |
+
{% elif item.type == 'todolist' %}<div class="item-preview"><i class="fa-solid fa-list-check"></i></div>
|
| 2204 |
+
{% elif item.type == 'shoppinglist' %}<div class="item-preview"><i class="fa-solid fa-cart-shopping"></i></div>
|
| 2205 |
{% elif item.type == 'file' %}
|
| 2206 |
{% if item.file_type == 'image' %}<img class="item-preview" src="{{ hf_file_url_jinja(item.path) }}" loading="lazy">
|
| 2207 |
{% elif item.file_type == 'video' %}<video class="item-preview" preload="metadata" muted loading="lazy"><source src="{{ hf_file_url_jinja(item.path, True) }}#t=0.5"></video>
|
|
|
|
| 2212 |
{% endif %}
|
| 2213 |
</div>
|
| 2214 |
<div class="item-name-info">
|
| 2215 |
+
<p class="item-name">{{ (item.title if item.type in ['note', 'todolist', 'shoppinglist'] else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}</p>
|
| 2216 |
{% if item.type == 'file' %}<p class="item-info">{{ item.upload_date }}</p>
|
| 2217 |
+
{% elif item.type in ['note', 'todolist', 'shoppinglist'] %}<p class="item-info">{{ item.modified_date }}</p>{% endif %}
|
| 2218 |
</div>
|
| 2219 |
<div class="item-actions-admin">
|
| 2220 |
<form action="{{ url_for('admin_delete_item', tma_user_id_str=user_id, item_id=item.id) }}" method="post" onsubmit="return confirm('Are you sure you want to delete this?');">
|
|
|
|
| 2261 |
downloadBtn.style.display = 'none';
|
| 2262 |
|
| 2263 |
try {
|
| 2264 |
+
if (type === 'note' || type === 'todolist' || type === 'shoppinglist') {
|
| 2265 |
+
const response = await fetch(`{{ url_for('admin_get_item', tma_user_id_str=user_id, item_id='__ID__') }}`.replace('__ID__', itemId));
|
| 2266 |
const data = await response.json();
|
| 2267 |
if(data.status === 'success') {
|
| 2268 |
+
let contentHTML = `<div style="padding:15px; text-align:left;"><h3>${data.item.title.replace(/</g,"<")}</h3><hr style="border-color:#333; margin:10px 0;">`;
|
| 2269 |
+
if (type === 'note') contentHTML += `<pre>${data.item.content.replace(/</g,"<")}</pre>`;
|
| 2270 |
+
if (type === 'todolist') contentHTML += `<ul>${data.item.tasks.map(t => `<li><input type='checkbox' ${t.completed ? 'checked' : ''} disabled> ${t.text}</li>`).join('')}</ul>`;
|
| 2271 |
+
if (type === 'shoppinglist') contentHTML += `<ul>${data.item.items.map(i => `<li><input type='checkbox' ${i.purchased ? 'checked' : ''} disabled> ${i.name} (x${i.quantity})</li>`).join('')}</ul>`;
|
| 2272 |
+
contentHTML += `</div>`;
|
| 2273 |
+
modalContent.innerHTML = contentHTML;
|
| 2274 |
} else { throw new Error(data.message); }
|
| 2275 |
} else if (type === 'image') {
|
| 2276 |
modalContent.innerHTML = `<img src="${srcOrUrl}">`;
|
|
|
|
| 2498 |
except Exception as e:
|
| 2499 |
return Response(f"Download error: {e}", 502)
|
| 2500 |
|
| 2501 |
+
@app.route('/admhosto/item/<tma_user_id_str>/<item_id>')
|
| 2502 |
@admin_browser_login_required
|
| 2503 |
+
def admin_get_item(tma_user_id_str, item_id):
|
| 2504 |
data = load_data()
|
| 2505 |
user_data = data['users'].get(tma_user_id_str)
|
| 2506 |
if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
| 2507 |
+
node, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 2508 |
+
if not node: return jsonify({'status': 'error', 'message': 'Item not found'}), 404
|
| 2509 |
+
return jsonify({'status': 'success', 'item': node})
|
|
|
|
| 2510 |
|
| 2511 |
@app.route('/admhosto/delete_item/<tma_user_id_str>/<item_id>', methods=['POST'])
|
| 2512 |
@admin_browser_login_required
|
|
|
|
| 2535 |
try: save_data(data); flash('File not found on server, removed from DB.')
|
| 2536 |
except Exception: flash('DB save error (file not on server).', 'error')
|
| 2537 |
except Exception as e: flash(f'Deletion error: {e}', 'error')
|
| 2538 |
+
elif node.get('type') in ['note', 'todolist', 'shoppinglist']:
|
| 2539 |
if remove_node(user_data['filesystem'], item_id)[0]:
|
| 2540 |
+
try: save_data(data); flash(f'{node.get("type").capitalize()} deleted.')
|
| 2541 |
+
except Exception: flash('DB update failed after item deletion.', 'error')
|
| 2542 |
elif node.get('type') == 'folder':
|
| 2543 |
if node.get('children'): flash('Folder is not empty.', 'error')
|
| 2544 |
else:
|