diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -14,45 +14,146 @@ from io import BytesIO import uuid import hmac import hashlib +from urllib.parse import unquote + +from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes + +# --- CONFIGURATION --- +BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") +ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_ID_HERE") # Replace with actual admin Telegram ID +FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "supersecretkey_telegram_mini_app_unique") +DATA_FILE = 'cloudeng_tg_data.json' +REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Default to your repo +HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE +UPLOAD_FOLDER = 'uploads_tg' +WEB_APP_URL = os.getenv("WEB_APP_URL") # e.g., https://yourdomain.com +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") -DATA_FILE = 'cloudeng_data_tg.json' -REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set in your environment -HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Ensure this is set -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE # Ensure this is set -ADMIN_TELEGRAM_IDS = os.getenv("ADMIN_TELEGRAM_IDS", "").split(',') # Comma-separated string of admin Telegram IDs -BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" +app.secret_key = FLASK_SECRET_KEY +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -UPLOAD_FOLDER = 'uploads_tg' -os.makedirs(UPLOAD_FOLDER, exist_ok=True) +# --- STYLES --- +BASE_STYLE = ''' +:root { + --primary: #0088cc; --secondary: #00ab6c; --accent: #536de6; + --background-light: #ffffff; --background-dark: #1c1c1e; + --card-bg: rgba(240, 240, 245, 0.95); --card-bg-dark: rgba(44, 44, 46, 0.95); + --text-light: #000000; --text-dark: #ffffff; --shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + --glass-bg: rgba(200, 200, 200, 0.2); --transition: all 0.3s ease; --delete-color: #ff3b30; + --folder-color: #ff9500; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.5; -webkit-font-smoothing: antialiased; } +body.dark { background: var(--background-dark); color: var(--text-dark); } +.container { margin: 0 auto; max-width: 100%; min-height: 100vh; padding: 15px; background: var(--background-light); overflow-x: hidden; } +body.dark .container { background: var(--background-dark); } +h1 { font-size: 1.8em; font-weight: 700; text-align: center; margin-bottom: 20px; color: var(--primary); } +h2 { font-size: 1.4em; margin-top: 25px; margin-bottom:10px; color: var(--text-light); } +body.dark h2 { color: var(--text-dark); } +h4 { font-size: 1em; margin-top: 12px; margin-bottom: 4px; color: var(--accent); } +ol, ul { margin-left: 20px; margin-bottom: 12px; } +li { margin-bottom: 4px; } +input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ccc; border-radius: 10px; background: var(--glass-bg); color: var(--text-light); font-size: 1em; } +body.dark input, body.dark textarea { border-color: #444; color: var(--text-dark); background: rgba(70,70,70,0.3); } +input:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); } +.btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: 0 4px 10px rgba(0,0,0,0.1); display: inline-block; text-decoration: none; margin-top: 4px; margin-right: 4px; text-align: center; } +.btn:hover { transform: translateY(-2px); background: #0077b3; box-shadow: 0 6px 15px rgba(0,0,0,0.15); } +.download-btn { background: var(--secondary); } +.download-btn:hover { background: #00965e; } +.delete-btn { background: var(--delete-color); } +.delete-btn:hover { background: #e03024; } +.folder-btn { background: var(--folder-color); } +.folder-btn:hover { background: #e08300; } +.flash { color: var(--primary); text-align: center; margin-bottom: 12px; padding: 8px; background: rgba(0, 136, 204, 0.1); border-radius: 8px; border: 1px solid var(--primary); } +.flash.error { color: var(--delete-color); background: rgba(255, 59, 48, 0.1); border-color: var(--delete-color); } +.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } +.user-list { margin-top: 15px; } +.user-item { padding: 12px; background: var(--card-bg); border-radius: 12px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } +body.dark .user-item { background: var(--card-bg-dark); } +.user-item:hover { transform: translateY(-3px); } +.user-item a { color: var(--primary); text-decoration: none; font-weight: 600; } +.user-item a:hover { color: var(--accent); } +.item { background: var(--card-bg); padding: 12px; border-radius: 12px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } +body.dark .item { background: var(--card-bg-dark); } +.item:hover { transform: translateY(-3px); } +.item-preview { width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} +.item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--folder-color); line-height: 100px; } +.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } +.item a { color: var(--primary); text-decoration: none; } +.item a:hover { color: var(--accent); } +.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } +.item-actions .btn { font-size: 0.8em; padding: 6px 10px; } +.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 2000; justify-content: center; align-items: center; } +.modal-content { max-width: 95%; max-height: 95%; background: var(--background-light); padding: 10px; border-radius: 12px; overflow: auto; position: relative; } +body.dark .modal-content { background: var(--card-bg-dark); } +.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 8px; } +.modal iframe { width: 90vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 12px; border-radius: 6px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} +body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } +.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.3); border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; } +body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.15); } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 8px; margin: 12px 0; display: none; position: relative; height: 18px; } +#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 8px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 18px; color: var(--text-dark); font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.2); } +.breadcrumbs { margin-bottom: 15px; font-size: 1em; } +.breadcrumbs a { color: var(--accent); text-decoration: none; } +.breadcrumbs a:hover { text-decoration: underline; } +.breadcrumbs span { margin: 0 4px; color: #999; } +.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 120px; } +.folder-actions .btn { margin: 0; flex-shrink: 0;} +#auth-message { text-align: center; padding: 20px; font-size: 1.2em; } +@media (max-width: 480px) { + .container { padding: 10px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; } + .item-preview { height: 80px; } + .item.folder .item-preview { font-size: 40px; line-height: 80px; } + h1 { font-size: 1.6em; } + .btn { padding: 10px 20px; font-size: 0.9em; } + .item-actions .btn { padding: 5px 8px; font-size: 0.75em;} + .folder-actions { flex-direction: column; align-items: stretch; } +} +''' -cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -logging.basicConfig(level=logging.INFO) +# --- HELPER FUNCTIONS --- +def verify_telegram_data(init_data_str, bot_token_val): + if not init_data_str or not bot_token_val: + return False + + parsed_data = {} + for pair in init_data_str.split('&'): + if '=' in pair: + key, value = pair.split('=', 1) + parsed_data[key] = unquote(value) + + received_hash = parsed_data.pop('hash', None) + if not received_hash: + return False + + data_check_arr = [f"{key}={value}" for key, value in sorted(parsed_data.items())] + data_check_string = "\n".join(data_check_arr) + + secret_key = hmac.new("WebAppData".encode(), bot_token_val.encode(), hashlib.sha256).digest() + calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + return calculated_hash == received_hash -def verify_telegram_auth(init_data_str, bot_token): - try: - params = dict(item.split("=", 1) for item in init_data_str.split("&")) - hash_received = params.pop("hash", None) - if not hash_received: - return None - - data_check_arr = [] - for key, value in sorted(params.items()): - data_check_arr.append(f"{key}={value}") - data_check_string = "\n".join(data_check_arr) - - secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == hash_received: - user_data = json.loads(params.get("user", "{}")) - return user_data - return None - except Exception as e: - logging.error(f"Telegram auth verification error: {e}") - return None +def get_tg_user_display_name(tg_user_obj): + if not tg_user_obj: return "Unknown User" + first_name = tg_user_obj.get('first_name', '') + last_name = tg_user_obj.get('last_name', '') + username = tg_user_obj.get('username') + + if first_name and last_name: return f"{first_name} {last_name}" + if first_name: return first_name + if username: return username + return f"User {tg_user_obj.get('id')}" def find_node_by_id(filesystem, node_id): if not filesystem: return None, None @@ -62,7 +163,7 @@ def find_node_by_id(filesystem, node_id): while queue: current_node, parent = queue.pop(0) if current_node.get('type') == 'folder' and 'children' in current_node: - for i, child in enumerate(current_node['children']): + for child in current_node['children']: if child.get('id') == node_id: return child, current_node if child.get('type') == 'folder': @@ -83,8 +184,13 @@ def remove_node(filesystem, node_id): if node_to_remove and parent_node and 'children' in parent_node: parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] return True + # Handle root node removal attempt if it's the target (should not happen for files/folders within root) + elif node_to_remove and node_id == filesystem.get('id') and not parent_node: + logger.error("Attempted to remove root node itself via remove_node, which is not supported this way.") + return False # Or handle as a special case if needed return False + def get_node_path_string(filesystem, node_id): path_list = [] current_id = node_id @@ -97,18 +203,11 @@ def get_node_path_string(filesystem, node_id): current_id = parent.get('id') if parent else None return " / ".join(reversed(path_list)) or "Root" -def initialize_user_filesystem_if_needed(user_data, user_id_for_path_unused): - if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): +def initialize_user_filesystem(user_data): + if 'filesystem' not in user_data: user_data['filesystem'] = { - "type": "folder", - "id": "root", - "name": "root", - "children": [] + "type": "folder", "id": "root", "name": "root", "children": [] } - # Removed old 'files' list migration logic as it's complex with TG ID change - # and relied on session['username'] which is not available globally in load_data. - # New users will get the basic structure. Existing data might need manual migration if format is very old. - @cache.memoize(timeout=300) def load_data(): @@ -116,16 +215,15 @@ def load_data(): download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - if not isinstance(data, dict): - logging.warning("Data is not in dict format, initializing empty database") - return {'users': {}} - data.setdefault('users', {}) - for user_id_str, user_data_item in data['users'].items(): - initialize_user_filesystem_if_needed(user_data_item, user_id_str) - logging.info("Data successfully loaded and initialized for TG app") - return data + if not isinstance(data, dict): + data = {'users': {}} + data.setdefault('users', {}) + for user_id, user_data_val in data['users'].items(): + initialize_user_filesystem(user_data_val) + logger.info("Data successfully loaded and initialized") + return data except Exception as e: - logging.error(f"Error loading data: {e}") + logger.error(f"Error loading data: {e}") return {'users': {}} def save_data(data): @@ -134,32 +232,29 @@ def save_data(data): json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() cache.clear() - logging.info("Data saved and uploaded to HF for TG app") + logger.info("Data saved and uploaded to HF") except Exception as e: - logging.error(f"Error saving data: {e}") + logger.error(f"Error saving data: {e}") raise def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") + logger.warning("HF_TOKEN_WRITE not set, skipping database upload.") return try: api = HfApi() api.upload_file( - path_or_fileobj=DATA_FILE, - path_in_repo=DATA_FILE, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Backup TG App {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Database uploaded to Hugging Face (TG App)") + logger.info("Database uploaded to Hugging Face") except Exception as e: - logging.error(f"Error uploading database (TG App): {e}") + logger.error(f"Error uploading database: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set, skipping database download.") + logger.warning("HF_TOKEN_READ not set, skipping database download.") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) return @@ -168,18 +263,21 @@ def download_db_from_hf(): repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False ) - logging.info("Database downloaded from Hugging Face (TG App)") + logger.info("Database downloaded from Hugging Face") + except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): + logger.warning(f"{DATA_FILE} or repo {REPO_ID} not found. Initializing empty database.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) except Exception as e: - logging.error(f"Error downloading database (TG App): {e}") + logger.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) def periodic_backup(): while True: - time.sleep(1800) # Backup every 30 minutes + time.sleep(1800) upload_db_to_hf() - def get_file_type(filename): filename_lower = filename.lower() if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' @@ -188,275 +286,436 @@ def get_file_type(filename): elif filename_lower.endswith('.txt'): return 'text' return 'other' -BASE_STYLE = ''' -:root { - --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; - --background-light: #f5f6fa; --background-dark: #1a1625; - --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); - --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); - --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; - --folder-color: #ffc107; -} -* { margin: 0; padding: 0; box-sizing: border-box; } -body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } -body.dark { background: var(--background-dark); color: var(--text-dark); } -.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } -body.dark .container { background: var(--card-bg-dark); } -h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } -h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); } -body.dark h2 { color: var(--text-dark); } -h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); } -ol, ul { margin-left: 20px; margin-bottom: 15px; } -li { margin-bottom: 5px; } -input, textarea { width: 100%; padding: 14px; margin: 12px 0; border: none; border-radius: 14px; background: var(--glass-bg); color: var(--text-light); font-size: 1.1em; box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); } -body.dark input, body.dark textarea { color: var(--text-dark); } -input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); } -.btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; } -.btn:hover { transform: scale(1.05); background: #e6415f; } -.download-btn { background: var(--secondary); } -.download-btn:hover { background: #00b8c5; } -.delete-btn { background: var(--delete-color); } -.delete-btn:hover { background: #cc3333; } -.folder-btn { background: var(--folder-color); } -.folder-btn:hover { background: #e6a000; } -.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; } -.flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); } -.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } -.user-list { margin-top: 20px; } -.user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); } -body.dark .user-item { background: var(--card-bg-dark); } -.user-item:hover { transform: translateY(-5px); } -.user-item a { color: var(--primary); text-decoration: none; font-weight: 600; } -.user-item a:hover { color: var(--accent); } -.item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } -body.dark .item { background: var(--card-bg-dark); } -.item:hover { transform: translateY(-5px); } -.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} -.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } -.item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } -.item a { color: var(--primary); text-decoration: none; } -.item a:hover { color: var(--accent); } -.item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; } -.item-actions .btn { font-size: 0.9em; padding: 5px 10px; } -.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; } -.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; } -body.dark .modal-content { background: var(--card-bg-dark); } -.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } -.modal iframe { width: 80vw; height: 85vh; border: none; } -.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} -body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } -.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; } -body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); } -#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } -#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } -#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } -.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } -.breadcrumbs a { color: var(--accent); text-decoration: none; } -.breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 5px; color: #aaa; } -.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } -.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; } -.folder-actions .btn { margin: 0; flex-shrink: 0;} -@media (max-width: 768px) { - .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } - .folder-actions { flex-direction: column; align-items: stretch; } - .folder-actions input[type=text] { width: 100%; } - .item-preview { height: 100px; } - .item.folder .item-preview { font-size: 50px; line-height: 100px; } - h1 { font-size: 1.8em; } - .btn { padding: 12px 24px; font-size: 1em; } - .item-actions .btn { padding: 4px 8px; font-size: 0.8em;} -} -@media (max-width: 480px) { - .container { padding: 15px; } - .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; } - .item-preview { height: 80px; } - .item.folder .item-preview { font-size: 40px; line-height: 80px; } - .item p { font-size: 0.8em;} - .breadcrumbs { font-size: 1em; } - .btn { padding: 10px 20px; } -} -''' +def is_admin_user(): + return session.get('telegram_user_id') and str(session.get('telegram_user_id')) == str(ADMIN_TELEGRAM_ID) + +# --- HTML TEMPLATES --- +APP_SHELL_HTML = """ + +
+ +Пользователь: {{ user_display_name }}
+{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %}{% for category, message in messages %}{{ item.name }}
+{{ item.original_filename | truncate(25, True) }}
+{{ item.upload_date }}
+Эта папка пуста.
{% endif %} +Это приложение предназначено для использования внутри Telegram.
-Пожалуйста, откройте его через вашего Telegram бота.
-Зарегистрирован: {{ user.created_at }}
+Файлов: {{ user.file_count }}
+Пользователей нет.
{% endfor %}{{ file.original_filename | truncate(30) }}
+В папке: {{ file.parent_path_str }}
+Загружен: {{ file.upload_date }}
+ID: {{ file.id }}
+Path: {{ file.path }}
+У пользователя нет файлов.
{% endfor %} +Загрузка...
'; modal.style.display = 'flex'; + try { + if (type === 'image') modalContent.innerHTML = `${escapedText}`;
+ } else modalContent.innerHTML = 'Предпросмотр для этого типа файла не поддерживается.
'; + } catch (error) { console.error("Error loading modal content:", error); modalContent.innerHTML = `Не удалось загрузить содержимое для предпросмотра. ${error.message}
`; } + } + function closeModal(event) { if (event.target === document.getElementById('mediaModal')) closeModalManual(); } + function closeModalManual() { + const modal = document.getElementById('mediaModal'); modal.style.display = 'none'; + const video = modal.querySelector('video'); if (video) video.pause(); + const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = 'about:blank'; + document.getElementById('modalContent').innerHTML = ''; + } + if (window.Telegram && window.Telegram.WebApp) { window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('admin_panel') }}"; }); window.Telegram.WebApp.BackButton.show(); } +