Update app.py
Browse files
app.py
CHANGED
|
@@ -117,7 +117,7 @@ def register():
|
|
| 117 |
flash('Пользователь уже существует!')
|
| 118 |
return redirect(url_for('register'))
|
| 119 |
|
| 120 |
-
data['users'][username] = {'password': password}
|
| 121 |
save_data(data)
|
| 122 |
logging.info(f"Пользователь {username} зарегистрирован")
|
| 123 |
flash('Регистрация успешна! Войдите в систему.')
|
|
@@ -209,7 +209,7 @@ def login():
|
|
| 209 |
username = session.get('username', None)
|
| 210 |
html = '''
|
| 211 |
<!DOCTYPE html>
|
| 212 |
-
<html lang="
|
| 213 |
<head>
|
| 214 |
<meta charset="UTF-8">
|
| 215 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -478,7 +478,7 @@ def post_page(post_id):
|
|
| 478 |
|
| 479 |
html = '''
|
| 480 |
<!DOCTYPE html>
|
| 481 |
-
<html lang="
|
| 482 |
<head>
|
| 483 |
<meta charset="UTF-8">
|
| 484 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -636,18 +636,56 @@ def profile():
|
|
| 636 |
username = session['username']
|
| 637 |
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
|
| 638 |
is_authenticated = 'username' in session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
if request.method == 'POST':
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
save_data(data)
|
| 645 |
-
logging.info(f"
|
| 646 |
return redirect(url_for('profile'))
|
| 647 |
|
| 648 |
html = '''
|
| 649 |
<!DOCTYPE html>
|
| 650 |
-
<html lang="
|
| 651 |
<head>
|
| 652 |
<meta charset="UTF-8">
|
| 653 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -669,13 +707,19 @@ def profile():
|
|
| 669 |
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
|
| 670 |
.post-item:hover { transform: scale(1.02); }
|
| 671 |
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; }
|
| 672 |
-
.btn { display: block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; }
|
| 673 |
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); }
|
| 674 |
.delete-btn { background: rgba(239, 68, 68, 0.9); }
|
| 675 |
.delete-btn:hover { background: rgba(239, 68, 68, 1); }
|
| 676 |
.stats { font-size: 0.9em; color: #666; margin-top: 5px; }
|
| 677 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
|
| 678 |
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
@media (max-width: 768px) {
|
| 680 |
.sidebar { transform: translateX(-100%); width: 200px; }
|
| 681 |
.sidebar.active { transform: translateX(0); }
|
|
@@ -686,6 +730,7 @@ def profile():
|
|
| 686 |
.post-preview { height: 150px; }
|
| 687 |
.btn { font-size: 14px; padding: 10px; }
|
| 688 |
h1, h2 { font-size: 1.5em; }
|
|
|
|
| 689 |
}
|
| 690 |
</style>
|
| 691 |
</head>
|
|
@@ -694,6 +739,24 @@ def profile():
|
|
| 694 |
''' + NAV_HTML + '''
|
| 695 |
<div class="container">
|
| 696 |
<h1>Профиль: {{ username }}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
<a href="{{ url_for('upload') }}" class="btn">Добавить публикацию</a>
|
| 698 |
<h2>Ваши публикации</h2>
|
| 699 |
<div class="post-grid">
|
|
@@ -715,7 +778,7 @@ def profile():
|
|
| 715 |
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p>
|
| 716 |
<form method="POST">
|
| 717 |
<input type="hidden" name="post_id" value="{{ post['id'] }}">
|
| 718 |
-
<button type="submit" class="btn delete-btn">Удалить</button>
|
| 719 |
</form>
|
| 720 |
</div>
|
| 721 |
{% endfor %}
|
|
@@ -748,6 +811,13 @@ def profile():
|
|
| 748 |
}
|
| 749 |
}
|
| 750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
document.addEventListener('DOMContentLoaded', function() {
|
| 752 |
const videos = document.querySelectorAll('.post-preview');
|
| 753 |
videos.forEach(video => {
|
|
@@ -803,7 +873,7 @@ def profile():
|
|
| 803 |
</body>
|
| 804 |
</html>
|
| 805 |
'''
|
| 806 |
-
return render_template_string(html, username=username, user_posts=user_posts, repo_id=REPO_ID, is_authenticated=is_authenticated)
|
| 807 |
|
| 808 |
# Страница профиля другого пользователя
|
| 809 |
@app.route('/profile/<username>')
|
|
@@ -815,10 +885,20 @@ def user_profile(username):
|
|
| 815 |
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
|
| 816 |
is_authenticated = 'username' in session
|
| 817 |
current_user = session.get('username', None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
|
| 819 |
html = '''
|
| 820 |
<!DOCTYPE html>
|
| 821 |
-
<html lang="
|
| 822 |
<head>
|
| 823 |
<meta charset="UTF-8">
|
| 824 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -840,13 +920,17 @@ def user_profile(username):
|
|
| 840 |
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
|
| 841 |
.post-item:hover { transform: scale(1.02); }
|
| 842 |
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; }
|
| 843 |
-
.btn { display: block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; }
|
| 844 |
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); }
|
| 845 |
.stats { font-size: 0.9em; color: #666; margin-top: 5px; }
|
| 846 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
|
| 847 |
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; }
|
| 848 |
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; }
|
| 849 |
.username-link:hover { text-decoration: underline; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
@media (max-width: 768px) {
|
| 851 |
.sidebar { transform: translateX(-100%); width: 200px; }
|
| 852 |
.sidebar.active { transform: translateX(0); }
|
|
@@ -857,6 +941,7 @@ def user_profile(username):
|
|
| 857 |
.post-preview { height: 150px; }
|
| 858 |
.btn { font-size: 14px; padding: 10px; }
|
| 859 |
h1, h2 { font-size: 1.5em; }
|
|
|
|
| 860 |
}
|
| 861 |
</style>
|
| 862 |
</head>
|
|
@@ -865,6 +950,17 @@ def user_profile(username):
|
|
| 865 |
''' + NAV_HTML + '''
|
| 866 |
<div class="container">
|
| 867 |
<h1>Профиль: {{ username }}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
<h2>Публикации</h2>
|
| 869 |
<div class="post-grid">
|
| 870 |
{% if user_posts %}
|
|
@@ -914,6 +1010,13 @@ def user_profile(username):
|
|
| 914 |
}
|
| 915 |
}
|
| 916 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
document.addEventListener('DOMContentLoaded', function() {
|
| 918 |
const videos = document.querySelectorAll('.post-preview');
|
| 919 |
videos.forEach(video => {
|
|
@@ -969,7 +1072,7 @@ def user_profile(username):
|
|
| 969 |
</body>
|
| 970 |
</html>
|
| 971 |
'''
|
| 972 |
-
return render_template_string(html, username=username, user_posts=user_posts, repo_id=REPO_ID, is_authenticated=is_authenticated, current_user=current_user)
|
| 973 |
|
| 974 |
# Страница загрузки контента
|
| 975 |
@app.route('/upload', methods=['GET', 'POST'])
|
|
@@ -1035,7 +1138,7 @@ def upload():
|
|
| 1035 |
username = session.get('username', None)
|
| 1036 |
html = '''
|
| 1037 |
<!DOCTYPE html>
|
| 1038 |
-
<html lang="
|
| 1039 |
<head>
|
| 1040 |
<meta charset="UTF-8">
|
| 1041 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -1047,7 +1150,7 @@ def upload():
|
|
| 1047 |
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
| 1048 |
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; }
|
| 1049 |
.nav-links { display: flex; flex-direction: column; gap: 10px; }
|
| 1050 |
-
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; }
|
| 1051 |
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); }
|
| 1052 |
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
|
| 1053 |
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; }
|
|
@@ -1124,37 +1227,37 @@ def upload():
|
|
| 1124 |
'''
|
| 1125 |
return render_template_string(html, username=username, is_authenticated=is_authenticated)
|
| 1126 |
|
| 1127 |
-
# Админ-панель (
|
| 1128 |
@app.route('/admhosto', methods=['GET', 'POST'])
|
| 1129 |
def admin_panel():
|
| 1130 |
data = load_data()
|
| 1131 |
-
|
| 1132 |
is_authenticated = 'username' in session
|
| 1133 |
username = session.get('username', None)
|
| 1134 |
|
| 1135 |
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' and 'search' in request.form else request.args.get('search', '').strip().lower()
|
| 1136 |
|
| 1137 |
if search_query:
|
| 1138 |
-
|
| 1139 |
|
| 1140 |
if request.method == 'POST' and 'delete' in request.form:
|
| 1141 |
post_id = request.form.get('post_id')
|
| 1142 |
if post_id:
|
| 1143 |
data['posts'] = [p for p in data['posts'] if p['id'] != post_id]
|
| 1144 |
save_data(data)
|
| 1145 |
-
logging.info(f"
|
| 1146 |
return redirect(url_for('admin_panel'))
|
| 1147 |
|
| 1148 |
html = '''
|
| 1149 |
<!DOCTYPE html>
|
| 1150 |
-
<html lang="
|
| 1151 |
<head>
|
| 1152 |
<meta charset="UTF-8">
|
| 1153 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1154 |
<title>Админ-панель</title>
|
| 1155 |
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
| 1156 |
<style>
|
| 1157 |
-
body { font-family: 'Poppins',
|
| 1158 |
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; }
|
| 1159 |
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
| 1160 |
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; }
|
|
@@ -1165,10 +1268,10 @@ def admin_panel():
|
|
| 1165 |
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; }
|
| 1166 |
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
| 1167 |
.container { max-width: 1200px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; }
|
| 1168 |
-
.
|
| 1169 |
-
.
|
| 1170 |
-
.
|
| 1171 |
-
.
|
| 1172 |
.delete-btn { padding: 10px; background: #ef4444; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; }
|
| 1173 |
.delete-btn:hover { background: #dc2626; }
|
| 1174 |
.search-container { margin-bottom: 20px; }
|
|
@@ -1183,9 +1286,9 @@ def admin_panel():
|
|
| 1183 |
.sidebar.active { transform: translateX(0); }
|
| 1184 |
.menu-btn { display: block; }
|
| 1185 |
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); }
|
| 1186 |
-
.
|
| 1187 |
-
.
|
| 1188 |
-
.
|
| 1189 |
.delete-btn, .search-input { padding: 10px; font-size: 14px; }
|
| 1190 |
.search-btn { padding: 10px 15px; font-size: 14px; }
|
| 1191 |
h1 { font-size: 1.5em; }
|
|
@@ -1196,34 +1299,38 @@ def admin_panel():
|
|
| 1196 |
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
|
| 1197 |
''' + NAV_HTML + '''
|
| 1198 |
<div class="container">
|
| 1199 |
-
<h1>Админ-панель: Все
|
| 1200 |
<div class="search-container">
|
| 1201 |
<form method="POST">
|
| 1202 |
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}">
|
| 1203 |
<button type="submit" class="search-btn">Искать</button>
|
| 1204 |
</form>
|
| 1205 |
</div>
|
| 1206 |
-
<div class="
|
| 1207 |
-
{% if
|
| 1208 |
-
{% for
|
| 1209 |
-
<div class="
|
| 1210 |
-
<a href="{{ url_for('post_page', post_id=
|
| 1211 |
-
|
| 1212 |
-
<
|
| 1213 |
-
|
| 1214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
</a>
|
| 1216 |
-
<p>{{
|
| 1217 |
-
<p>Загрузил: <a href="{{ url_for('user_profile', username=
|
| 1218 |
-
<p class="stats">Просмотров: {{
|
| 1219 |
<form method="POST">
|
| 1220 |
-
<input type="hidden" name="post_id" value="{{
|
| 1221 |
<button type="submit" name="delete" class="delete-btn">Удалить</button>
|
| 1222 |
</form>
|
| 1223 |
</div>
|
| 1224 |
{% endfor %}
|
| 1225 |
{% else %}
|
| 1226 |
-
<p
|
| 1227 |
{% endif %}
|
| 1228 |
</div>
|
| 1229 |
</div>
|
|
@@ -1233,20 +1340,22 @@ def admin_panel():
|
|
| 1233 |
}
|
| 1234 |
|
| 1235 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1236 |
-
const videos = document.querySelectorAll('.
|
| 1237 |
videos.forEach(video => {
|
| 1238 |
-
video.
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
|
|
|
|
|
|
| 1243 |
});
|
| 1244 |
});
|
| 1245 |
</script>
|
| 1246 |
</body>
|
| 1247 |
</html>
|
| 1248 |
'''
|
| 1249 |
-
return render_template_string(html,
|
| 1250 |
|
| 1251 |
if __name__ == '__main__':
|
| 1252 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
|
|
|
| 117 |
flash('Пользователь уже существует!')
|
| 118 |
return redirect(url_for('register'))
|
| 119 |
|
| 120 |
+
data['users'][username] = {'password': password, 'bio': '', 'link': '', 'avatar': None}
|
| 121 |
save_data(data)
|
| 122 |
logging.info(f"Пользователь {username} зарегистрирован")
|
| 123 |
flash('Регистрация успешна! Войдите в систему.')
|
|
|
|
| 209 |
username = session.get('username', None)
|
| 210 |
html = '''
|
| 211 |
<!DOCTYPE html>
|
| 212 |
+
<html lang="ру">
|
| 213 |
<head>
|
| 214 |
<meta charset="UTF-8">
|
| 215 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 478 |
|
| 479 |
html = '''
|
| 480 |
<!DOCTYPE html>
|
| 481 |
+
<html lang="ру">
|
| 482 |
<head>
|
| 483 |
<meta charset="UTF-8">
|
| 484 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 636 |
username = session['username']
|
| 637 |
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
|
| 638 |
is_authenticated = 'username' in session
|
| 639 |
+
|
| 640 |
+
# Подсчет общего количества просмотров и лайков
|
| 641 |
+
total_views = sum(post.get('views', 0) for post in user_posts)
|
| 642 |
+
total_likes = sum(len(post.get('likes', [])) for post in user_posts)
|
| 643 |
+
|
| 644 |
+
# Получение текущих данных пользователя
|
| 645 |
+
user_data = data['users'].get(username, {})
|
| 646 |
+
bio = user_data.get('bio', '')
|
| 647 |
+
link = user_data.get('link', '')
|
| 648 |
+
avatar = user_data.get('avatar', None)
|
| 649 |
|
| 650 |
if request.method == 'POST':
|
| 651 |
+
if 'delete_post' in request.form:
|
| 652 |
+
post_id = request.form.get('post_id')
|
| 653 |
+
if post_id:
|
| 654 |
+
data['posts'] = [p for p in data['posts'] if p['id'] != post_id or p['uploader'] != username]
|
| 655 |
+
save_data(data)
|
| 656 |
+
logging.info(f"Публикация {post_id} удалена пользователем {username}")
|
| 657 |
+
return redirect(url_for('profile'))
|
| 658 |
+
elif 'update_profile' in request.form:
|
| 659 |
+
bio = request.form.get('bio', '')
|
| 660 |
+
link = request.form.get('link', '')
|
| 661 |
+
avatar_file = request.files.get('avatar')
|
| 662 |
+
if avatar_file and avatar_file.filename:
|
| 663 |
+
filename = secure_filename(avatar_file.filename)
|
| 664 |
+
temp_path = os.path.join('uploads', filename)
|
| 665 |
+
os.makedirs('uploads', exist_ok=True)
|
| 666 |
+
avatar_file.save(temp_path)
|
| 667 |
+
api = HfApi()
|
| 668 |
+
avatar_path = f"avatars/{username}/{filename}"
|
| 669 |
+
api.upload_file(
|
| 670 |
+
path_or_fileobj=temp_path,
|
| 671 |
+
path_in_repo=avatar_path,
|
| 672 |
+
repo_id=REPO_ID,
|
| 673 |
+
repo_type="dataset",
|
| 674 |
+
token=HF_TOKEN_WRITE,
|
| 675 |
+
commit_message=f"Добавлен аватар для {username}"
|
| 676 |
+
)
|
| 677 |
+
data['users'][username]['avatar'] = avatar_path
|
| 678 |
+
if os.path.exists(temp_path):
|
| 679 |
+
os.remove(temp_path)
|
| 680 |
+
data['users'][username]['bio'] = bio
|
| 681 |
+
data['users'][username]['link'] = link
|
| 682 |
save_data(data)
|
| 683 |
+
logging.info(f"Профиль {username} обновлен")
|
| 684 |
return redirect(url_for('profile'))
|
| 685 |
|
| 686 |
html = '''
|
| 687 |
<!DOCTYPE html>
|
| 688 |
+
<html lang="ру">
|
| 689 |
<head>
|
| 690 |
<meta charset="UTF-8">
|
| 691 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 707 |
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
|
| 708 |
.post-item:hover { transform: scale(1.02); }
|
| 709 |
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; }
|
| 710 |
+
.btn { display: inline-block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; }
|
| 711 |
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); }
|
| 712 |
.delete-btn { background: rgba(239, 68, 68, 0.9); }
|
| 713 |
.delete-btn:hover { background: rgba(239, 68, 68, 1); }
|
| 714 |
.stats { font-size: 0.9em; color: #666; margin-top: 5px; }
|
| 715 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
|
| 716 |
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; }
|
| 717 |
+
.avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; }
|
| 718 |
+
.profile-info { margin-bottom: 20px; }
|
| 719 |
+
textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; resize: vertical; background: rgba(255, 255, 255, 0.8); }
|
| 720 |
+
input[type="text"] { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; background: rgba(255, 255, 255, 0.8); }
|
| 721 |
+
.share-btn { background: rgba(16, 185, 129, 0.9); }
|
| 722 |
+
.share-btn:hover { background: rgba(16, 185, 129, 1); }
|
| 723 |
@media (max-width: 768px) {
|
| 724 |
.sidebar { transform: translateX(-100%); width: 200px; }
|
| 725 |
.sidebar.active { transform: translateX(0); }
|
|
|
|
| 730 |
.post-preview { height: 150px; }
|
| 731 |
.btn { font-size: 14px; padding: 10px; }
|
| 732 |
h1, h2 { font-size: 1.5em; }
|
| 733 |
+
.avatar { width: 80px; height: 80px; }
|
| 734 |
}
|
| 735 |
</style>
|
| 736 |
</head>
|
|
|
|
| 739 |
''' + NAV_HTML + '''
|
| 740 |
<div class="container">
|
| 741 |
<h1>Профиль: {{ username }}</h1>
|
| 742 |
+
<div class="profile-info">
|
| 743 |
+
{% if avatar %}
|
| 744 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar">
|
| 745 |
+
{% endif %}
|
| 746 |
+
<p>{{ bio }}</p>
|
| 747 |
+
{% if link %}
|
| 748 |
+
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p>
|
| 749 |
+
{% endif %}
|
| 750 |
+
<p>Всего просмотров: {{ total_views }} | Всего лайков: {{ total_likes }}</p>
|
| 751 |
+
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться профилем</button>
|
| 752 |
+
</div>
|
| 753 |
+
<h2>Редактировать профиль</h2>
|
| 754 |
+
<form method="POST" enctype="multipart/form-data">
|
| 755 |
+
<textarea name="bio" placeholder="Описание профиля" rows="3">{{ bio }}</textarea>
|
| 756 |
+
<input type="text" name="link" placeholder="Ссылка" value="{{ link }}">
|
| 757 |
+
<input type="file" name="avatar" accept="image/*">
|
| 758 |
+
<button type="submit" name="update_profile" class="btn">Сохранить</button>
|
| 759 |
+
</form>
|
| 760 |
<a href="{{ url_for('upload') }}" class="btn">Добавить публикацию</a>
|
| 761 |
<h2>Ваши публикации</h2>
|
| 762 |
<div class="post-grid">
|
|
|
|
| 778 |
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p>
|
| 779 |
<form method="POST">
|
| 780 |
<input type="hidden" name="post_id" value="{{ post['id'] }}">
|
| 781 |
+
<button type="submit" name="delete_post" class="btn delete-btn">Удалить</button>
|
| 782 |
</form>
|
| 783 |
</div>
|
| 784 |
{% endfor %}
|
|
|
|
| 811 |
}
|
| 812 |
}
|
| 813 |
|
| 814 |
+
function copyProfileLink() {
|
| 815 |
+
const url = window.location.href;
|
| 816 |
+
navigator.clipboard.writeText(url).then(() => {
|
| 817 |
+
alert('Ссылка на профиль скопирована в буфер обмена!');
|
| 818 |
+
});
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
document.addEventListener('DOMContentLoaded', function() {
|
| 822 |
const videos = document.querySelectorAll('.post-preview');
|
| 823 |
videos.forEach(video => {
|
|
|
|
| 873 |
</body>
|
| 874 |
</html>
|
| 875 |
'''
|
| 876 |
+
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated)
|
| 877 |
|
| 878 |
# Страница профиля другого пользователя
|
| 879 |
@app.route('/profile/<username>')
|
|
|
|
| 885 |
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
|
| 886 |
is_authenticated = 'username' in session
|
| 887 |
current_user = session.get('username', None)
|
| 888 |
+
|
| 889 |
+
# Подсчет общего количества просмотров и лайков
|
| 890 |
+
total_views = sum(post.get('views', 0) for post in user_posts)
|
| 891 |
+
total_likes = sum(len(post.get('likes', [])) for post in user_posts)
|
| 892 |
+
|
| 893 |
+
# Получение данных пользователя
|
| 894 |
+
user_data = data['users'].get(username, {})
|
| 895 |
+
bio = user_data.get('bio', '')
|
| 896 |
+
link = user_data.get('link', '')
|
| 897 |
+
avatar = user_data.get('avatar', None)
|
| 898 |
|
| 899 |
html = '''
|
| 900 |
<!DOCTYPE html>
|
| 901 |
+
<html lang="ру">
|
| 902 |
<head>
|
| 903 |
<meta charset="UTF-8">
|
| 904 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 920 |
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
|
| 921 |
.post-item:hover { transform: scale(1.02); }
|
| 922 |
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; }
|
| 923 |
+
.btn { display: inline-block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; }
|
| 924 |
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); }
|
| 925 |
.stats { font-size: 0.9em; color: #666; margin-top: 5px; }
|
| 926 |
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
|
| 927 |
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; }
|
| 928 |
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; }
|
| 929 |
.username-link:hover { text-decoration: underline; }
|
| 930 |
+
.avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; }
|
| 931 |
+
.profile-info { margin-bottom: 20px; }
|
| 932 |
+
.share-btn { background: rgba(16, 185, 129, 0.9); }
|
| 933 |
+
.share-btn:hover { background: rgba(16, 185, 129, 1); }
|
| 934 |
@media (max-width: 768px) {
|
| 935 |
.sidebar { transform: translateX(-100%); width: 200px; }
|
| 936 |
.sidebar.active { transform: translateX(0); }
|
|
|
|
| 941 |
.post-preview { height: 150px; }
|
| 942 |
.btn { font-size: 14px; padding: 10px; }
|
| 943 |
h1, h2 { font-size: 1.5em; }
|
| 944 |
+
.avatar { width: 80px; height: 80px; }
|
| 945 |
}
|
| 946 |
</style>
|
| 947 |
</head>
|
|
|
|
| 950 |
''' + NAV_HTML + '''
|
| 951 |
<div class="container">
|
| 952 |
<h1>Профиль: {{ username }}</h1>
|
| 953 |
+
<div class="profile-info">
|
| 954 |
+
{% if avatar %}
|
| 955 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar">
|
| 956 |
+
{% endif %}
|
| 957 |
+
<p>{{ bio }}</p>
|
| 958 |
+
{% if link %}
|
| 959 |
+
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p>
|
| 960 |
+
{% endif %}
|
| 961 |
+
<p>Всего просмотров: {{ total_views }} | Всего лайков: {{ total_likes }}</p>
|
| 962 |
+
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться профилем</button>
|
| 963 |
+
</div>
|
| 964 |
<h2>Публикации</h2>
|
| 965 |
<div class="post-grid">
|
| 966 |
{% if user_posts %}
|
|
|
|
| 1010 |
}
|
| 1011 |
}
|
| 1012 |
|
| 1013 |
+
function copyProfileLink() {
|
| 1014 |
+
const url = window.location.href;
|
| 1015 |
+
navigator.clipboard.writeText(url).then(() => {
|
| 1016 |
+
alert('Ссылка на профиль скопирована в буфер обмена!');
|
| 1017 |
+
});
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1021 |
const videos = document.querySelectorAll('.post-preview');
|
| 1022 |
videos.forEach(video => {
|
|
|
|
| 1072 |
</body>
|
| 1073 |
</html>
|
| 1074 |
'''
|
| 1075 |
+
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, current_user=current_user)
|
| 1076 |
|
| 1077 |
# Страница загрузки контента
|
| 1078 |
@app.route('/upload', methods=['GET', 'POST'])
|
|
|
|
| 1138 |
username = session.get('username', None)
|
| 1139 |
html = '''
|
| 1140 |
<!DOCTYPE html>
|
| 1141 |
+
<html lang="ру">
|
| 1142 |
<head>
|
| 1143 |
<meta charset="UTF-8">
|
| 1144 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 1150 |
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
| 1151 |
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; }
|
| 1152 |
.nav-links { display: flex; flex-direction: column; gap: 10px; }
|
| 1153 |
+
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: # 3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; }
|
| 1154 |
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); }
|
| 1155 |
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
|
| 1156 |
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; }
|
|
|
|
| 1227 |
'''
|
| 1228 |
return render_template_string(html, username=username, is_authenticated=is_authenticated)
|
| 1229 |
|
| 1230 |
+
# Админ-панель (все посты: видео и фото)
|
| 1231 |
@app.route('/admhosto', methods=['GET', 'POST'])
|
| 1232 |
def admin_panel():
|
| 1233 |
data = load_data()
|
| 1234 |
+
posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True)
|
| 1235 |
is_authenticated = 'username' in session
|
| 1236 |
username = session.get('username', None)
|
| 1237 |
|
| 1238 |
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' and 'search' in request.form else request.args.get('search', '').strip().lower()
|
| 1239 |
|
| 1240 |
if search_query:
|
| 1241 |
+
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()]
|
| 1242 |
|
| 1243 |
if request.method == 'POST' and 'delete' in request.form:
|
| 1244 |
post_id = request.form.get('post_id')
|
| 1245 |
if post_id:
|
| 1246 |
data['posts'] = [p for p in data['posts'] if p['id'] != post_id]
|
| 1247 |
save_data(data)
|
| 1248 |
+
logging.info(f"Удален пост с ID {post_id} через админ-панель")
|
| 1249 |
return redirect(url_for('admin_panel'))
|
| 1250 |
|
| 1251 |
html = '''
|
| 1252 |
<!DOCTYPE html>
|
| 1253 |
+
<html lang="ру">
|
| 1254 |
<head>
|
| 1255 |
<meta charset="UTF-8">
|
| 1256 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1257 |
<title>Админ-панель</title>
|
| 1258 |
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
| 1259 |
<style>
|
| 1260 |
+
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; }
|
| 1261 |
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; }
|
| 1262 |
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
| 1263 |
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; }
|
|
|
|
| 1268 |
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; }
|
| 1269 |
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
| 1270 |
.container { max-width: 1200px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; }
|
| 1271 |
+
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
| 1272 |
+
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; }
|
| 1273 |
+
.post-item:hover { transform: scale(1.02); }
|
| 1274 |
+
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; }
|
| 1275 |
.delete-btn { padding: 10px; background: #ef4444; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; }
|
| 1276 |
.delete-btn:hover { background: #dc2626; }
|
| 1277 |
.search-container { margin-bottom: 20px; }
|
|
|
|
| 1286 |
.sidebar.active { transform: translateX(0); }
|
| 1287 |
.menu-btn { display: block; }
|
| 1288 |
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); }
|
| 1289 |
+
.post-grid { grid-template-columns: 1fr; gap: 15px; }
|
| 1290 |
+
.post-item { padding: 10px; }
|
| 1291 |
+
.post-preview { height: 150px; }
|
| 1292 |
.delete-btn, .search-input { padding: 10px; font-size: 14px; }
|
| 1293 |
.search-btn { padding: 10px 15px; font-size: 14px; }
|
| 1294 |
h1 { font-size: 1.5em; }
|
|
|
|
| 1299 |
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
|
| 1300 |
''' + NAV_HTML + '''
|
| 1301 |
<div class="container">
|
| 1302 |
+
<h1>Админ-панель: Все посты</h1>
|
| 1303 |
<div class="search-container">
|
| 1304 |
<form method="POST">
|
| 1305 |
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}">
|
| 1306 |
<button type="submit" class="search-btn">Искать</button>
|
| 1307 |
</form>
|
| 1308 |
</div>
|
| 1309 |
+
<div class="post-grid">
|
| 1310 |
+
{% if posts %}
|
| 1311 |
+
{% for post in posts %}
|
| 1312 |
+
<div class="post-item">
|
| 1313 |
+
<a href="{{ url_for('post_page', post_id=post['id']) }}">
|
| 1314 |
+
{% if post['type'] == 'video' %}
|
| 1315 |
+
<video class="post-preview" preload="metadata" muted>
|
| 1316 |
+
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4">
|
| 1317 |
+
</video>
|
| 1318 |
+
{% else %}
|
| 1319 |
+
<img class="post-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}">
|
| 1320 |
+
{% endif %}
|
| 1321 |
+
<h2>{{ post['title'] }}</h2>
|
| 1322 |
</a>
|
| 1323 |
+
<p>{{ post['description'] }}</p>
|
| 1324 |
+
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p>
|
| 1325 |
+
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p>
|
| 1326 |
<form method="POST">
|
| 1327 |
+
<input type="hidden" name="post_id" value="{{ post['id'] }}">
|
| 1328 |
<button type="submit" name="delete" class="delete-btn">Удалить</button>
|
| 1329 |
</form>
|
| 1330 |
</div>
|
| 1331 |
{% endfor %}
|
| 1332 |
{% else %}
|
| 1333 |
+
<p>Посты не найдены.</p>
|
| 1334 |
{% endif %}
|
| 1335 |
</div>
|
| 1336 |
</div>
|
|
|
|
| 1340 |
}
|
| 1341 |
|
| 1342 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1343 |
+
const videos = document.querySelectorAll('.post-preview');
|
| 1344 |
videos.forEach(video => {
|
| 1345 |
+
if (video.tagName === 'VIDEO') {
|
| 1346 |
+
video.addEventListener('loadedmetadata', function() {
|
| 1347 |
+
const duration = video.duration;
|
| 1348 |
+
const randomTime = Math.random() * duration;
|
| 1349 |
+
video.currentTime = randomTime;
|
| 1350 |
+
});
|
| 1351 |
+
}
|
| 1352 |
});
|
| 1353 |
});
|
| 1354 |
</script>
|
| 1355 |
</body>
|
| 1356 |
</html>
|
| 1357 |
'''
|
| 1358 |
+
return render_template_string(html, posts=posts, is_authenticated=is_authenticated, username=username, repo_id=REPO_ID, search_query=search_query)
|
| 1359 |
|
| 1360 |
if __name__ == '__main__':
|
| 1361 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|