657 lines
28 KiB
Python
657 lines
28 KiB
Python
|
|
# Пути к файлам в контейнере
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
# Базовый путь для данных
|
|||
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
|
|||
|
|
# Путь к файлу для хранения resolutions
|
|||
|
|
CSV_FILE = os.path.join(BASE_DIR, 'resolutions.csv')
|
|||
|
|
|
|||
|
|
# Используем OpenSans шрифт
|
|||
|
|
FONT_PATH = os.path.join(BASE_DIR, 'opensans.ttf')
|
|||
|
|
|
|||
|
|
# Создание папки для галереи
|
|||
|
|
GALLERY_DIR = os.path.join(BASE_DIR, 'gallery')
|
|||
|
|
os.makedirs(GALLERY_DIR, exist_ok=True)
|
|||
|
|
|
|||
|
|
# Пути к шаблонам
|
|||
|
|
TEMPLATES = {
|
|||
|
|
"Еж-тян": os.path.join(BASE_DIR, "ejtan.png"),
|
|||
|
|
"Ежунья": os.path.join(BASE_DIR, "ejunya.png"),
|
|||
|
|
"Оливьешка": os.path.join(BASE_DIR, "olive.png"),
|
|||
|
|
"Опер-тян": os.path.join(BASE_DIR, "opertan.png"),
|
|||
|
|
"Фортран & Co": os.path.join(BASE_DIR, "fortranco.png"),
|
|||
|
|
"Фунтик": os.path.join(BASE_DIR, "funtik.png")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
import streamlit as st
|
|||
|
|
import pandas as pd
|
|||
|
|
import random
|
|||
|
|
from datetime import datetime
|
|||
|
|
from PIL import Image, ImageDraw, ImageFont
|
|||
|
|
import shutil
|
|||
|
|
import base64
|
|||
|
|
|
|||
|
|
# Функция для инициализации CSV
|
|||
|
|
def initialize_csv():
|
|||
|
|
df = pd.DataFrame(columns=['Resolution', 'Category', 'Mascot', 'Timestamp', 'ImagePath'])
|
|||
|
|
df.to_csv(CSV_FILE, index=False)
|
|||
|
|
|
|||
|
|
# Проверка и инициализация CSV
|
|||
|
|
if not os.path.exists(CSV_FILE) or os.path.getsize(CSV_FILE) == 0:
|
|||
|
|
initialize_csv()
|
|||
|
|
else:
|
|||
|
|
try:
|
|||
|
|
pd.read_csv(CSV_FILE)
|
|||
|
|
except (pd.errors.EmptyDataError, pd.errors.ParserError):
|
|||
|
|
initialize_csv()
|
|||
|
|
|
|||
|
|
# Список категорий и маскотов
|
|||
|
|
CATEGORIES = ["Новогоднее поздравление", "Ебать ты молодец", "Эпик фейл"]
|
|||
|
|
MASCOTS = ["Еж-тян", "Ежунья", "Оливьешка", "Опер-тян", "Фортран & Co", "Фунтик"]
|
|||
|
|
|
|||
|
|
# Проверка существования файлов шаблонов
|
|||
|
|
def check_templates():
|
|||
|
|
missing_templates = []
|
|||
|
|
for name, path in TEMPLATES.items():
|
|||
|
|
if not os.path.exists(path):
|
|||
|
|
missing_templates.append(f"{name}: {path}")
|
|||
|
|
return missing_templates
|
|||
|
|
|
|||
|
|
# Проверка шрифта
|
|||
|
|
def check_font():
|
|||
|
|
if not os.path.exists(FONT_PATH):
|
|||
|
|
return False
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# Пул заголовков для каждой категории
|
|||
|
|
title_pools = {
|
|||
|
|
"Новогоднее поздравление": [
|
|||
|
|
"Заверяющий официальное пожелание анона",
|
|||
|
|
"Подтверждает новогоднее настроение",
|
|||
|
|
"Вручается в честь наступления очередного витка этой вселенной",
|
|||
|
|
"Официально разрешает радоваться Новому году",
|
|||
|
|
"Подтверждает наличие оливьешки",
|
|||
|
|
"На законное распитие всякого под бой курантов",
|
|||
|
|
"За сохранение новогоднего духа в условиях треда",
|
|||
|
|
"Выдан без очереди, потому что новый год",
|
|||
|
|
"Сертифицирует тёплые пожелания от анона к анону",
|
|||
|
|
"На выдачу новогоднего настроения в треде"
|
|||
|
|
],
|
|||
|
|
"Ебать ты молодец": [
|
|||
|
|
"Выдаётся охуенному молодцу",
|
|||
|
|
"Выдаётся по причине охуеть ты молодец",
|
|||
|
|
"Вручается за ультимативную годноту",
|
|||
|
|
"Вручается за запредельный уровень молодцовости",
|
|||
|
|
"Подтверждает факт: ты реально ебать какой молодец",
|
|||
|
|
"Выдан ебать какому молодцу",
|
|||
|
|
"За действия, от которых аноны охуели",
|
|||
|
|
"Официальное признание: охуенно справился",
|
|||
|
|
"За вклад в общее дело и локальный ахуй",
|
|||
|
|
"За демонстрацию силы постинга и абсолютной годноты"
|
|||
|
|
],
|
|||
|
|
"Эпик фейл": [
|
|||
|
|
"За полнейший пиздец",
|
|||
|
|
"Вручается тотальному серуну",
|
|||
|
|
"Вручается за эталонный проёб всего и сразу",
|
|||
|
|
"О фиксации эпик фейла",
|
|||
|
|
"За вклад в фонд коллективного серунства",
|
|||
|
|
"Подтверждает: так обосраться - это надо уметь",
|
|||
|
|
"За достижение дна и уверенное его пробитие",
|
|||
|
|
"О признании фейлом года без права апелляции",
|
|||
|
|
"За демонстрацию того, как делать НЕ надо",
|
|||
|
|
"Официальное признание постера позором Ежача"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Пул комментариев для каждой категории
|
|||
|
|
comment_pools = {
|
|||
|
|
"Новогоднее поздравление": [
|
|||
|
|
"Всем Ежачом верим, что так и будет!",
|
|||
|
|
"Зафиксировано и заверенно, теперь сбудется, инфа 146%",
|
|||
|
|
"Эти пожелания искренны, я гарантирую это!",
|
|||
|
|
"Заверено Оператором, так что точно сбудется",
|
|||
|
|
"Пожелания занесены в аналлы Ежача",
|
|||
|
|
"Ежач видел - значит так тому и быть",
|
|||
|
|
"Так и будет, сомневаться запрещено",
|
|||
|
|
"Записано, сохранено, пути назад нет",
|
|||
|
|
"Теперь это официальная реальность (в разработке)",
|
|||
|
|
"С этого момента считается пророчеством"
|
|||
|
|
],
|
|||
|
|
"Ебать ты молодец": [
|
|||
|
|
"Так держать!",
|
|||
|
|
"Ты заслужил уважение всех анонов",
|
|||
|
|
"Это было эпично!",
|
|||
|
|
"Такого ещё не видел никто",
|
|||
|
|
"Твой подвиг войдет в историю",
|
|||
|
|
"Оче годно, одобряемо всем тредом",
|
|||
|
|
"Мощно, анон",
|
|||
|
|
"Это был просто отвал жопы",
|
|||
|
|
"Восхищаемся всем Ежачом",
|
|||
|
|
"Это просто вау"
|
|||
|
|
|
|||
|
|
],
|
|||
|
|
"Эпик фейл": [
|
|||
|
|
"Ты обосрался на глазах у всего треда",
|
|||
|
|
"Господи, зачем ты это сделал",
|
|||
|
|
"Аноны запомнят это на долгие годы",
|
|||
|
|
"Таких фейлов у нас ещё не было",
|
|||
|
|
"Даже Оператору стыдно",
|
|||
|
|
"Интернет всё помнит",
|
|||
|
|
"Аноны в ахуе",
|
|||
|
|
"Поздравляем, ты вошёл не в ту историю",
|
|||
|
|
"Ну ты и выдал",
|
|||
|
|
"Лучше больше не пиши"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Пул нижнего текста для каждой категории
|
|||
|
|
signature_pools = {
|
|||
|
|
"Новогоднее поздравление": [
|
|||
|
|
"С Новым 2026 годом, анон!",
|
|||
|
|
"Желаем исполнения желаний в 2026!",
|
|||
|
|
"До встречи в новом году!",
|
|||
|
|
"Пусть все обещания сбудутся!",
|
|||
|
|
"С наступающим, анончик!",
|
|||
|
|
"Новый год уже здесь!",
|
|||
|
|
"2026 начинается прямо сейчас!",
|
|||
|
|
"Береги себя в новом году!",
|
|||
|
|
"С теплом, от анона к анону!",
|
|||
|
|
"Увидимся в 2026!"
|
|||
|
|
],
|
|||
|
|
"Ебать ты молодец": [
|
|||
|
|
"Так держать и дальше!",
|
|||
|
|
"Продолжай в том же духе!",
|
|||
|
|
"Ты - легенда!",
|
|||
|
|
"Гордимся тобой, анон!",
|
|||
|
|
"За тобой будущее Ежача!",
|
|||
|
|
"Это было красиво!",
|
|||
|
|
"Ежач одобряе!",
|
|||
|
|
"Достойно!",
|
|||
|
|
"Не останавливайся!",
|
|||
|
|
"Уважение заслужено!"
|
|||
|
|
],
|
|||
|
|
"Эпик фейл": [
|
|||
|
|
"Позор зафиксирован",
|
|||
|
|
"Это не забудут",
|
|||
|
|
"Удалить уже не получится",
|
|||
|
|
"С этим тебе жить",
|
|||
|
|
"Фейл принят без возражений",
|
|||
|
|
"Не пиши больше",
|
|||
|
|
"Это просто пиздец",
|
|||
|
|
"Невероятный кринж",
|
|||
|
|
"Смеялись всем тредом",
|
|||
|
|
"Борды - это не твоё"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Определение текстовых областей для каждой позиции
|
|||
|
|
# Формат: (x, y, max_width, line_height)
|
|||
|
|
text_areas = {
|
|||
|
|
"title": (840, 610, 800, 50), # Заголовок
|
|||
|
|
"content": (840, 740, 800, 40), # Основной текст
|
|||
|
|
"comment": (840, 1120, 800, 40), # Комментарий
|
|||
|
|
"signature": (1110, 1260, 600, 35) # Подпись/нижний текст
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Фиксированные размеры шрифтов
|
|||
|
|
font_sizes = {
|
|||
|
|
"title": 40,
|
|||
|
|
"content": 34,
|
|||
|
|
"comment": 34,
|
|||
|
|
"signature": 28
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Цвет текста
|
|||
|
|
FONT_COLOR = (232, 201, 165)
|
|||
|
|
|
|||
|
|
def wrap_text(text, font, max_width):
|
|||
|
|
"""Разбивает текст на строки, чтобы он помещался в заданную ширину"""
|
|||
|
|
lines = []
|
|||
|
|
|
|||
|
|
# Если текст уже содержит переносы строк, обрабатываем каждую часть отдельно
|
|||
|
|
text_parts = text.split('\n')
|
|||
|
|
|
|||
|
|
for part in text_parts:
|
|||
|
|
words = part.split()
|
|||
|
|
current_line = ""
|
|||
|
|
|
|||
|
|
for word in words:
|
|||
|
|
# Проверяем ширину текущей строки с новым словом
|
|||
|
|
test_line = f"{current_line} {word}" if current_line else word
|
|||
|
|
test_bbox = font.getbbox(test_line)
|
|||
|
|
test_width = test_bbox[2] - test_bbox[0]
|
|||
|
|
|
|||
|
|
if test_width <= max_width:
|
|||
|
|
current_line = test_line
|
|||
|
|
else:
|
|||
|
|
if current_line:
|
|||
|
|
lines.append(current_line)
|
|||
|
|
current_line = word
|
|||
|
|
|
|||
|
|
if current_line:
|
|||
|
|
lines.append(current_line)
|
|||
|
|
|
|||
|
|
return lines
|
|||
|
|
|
|||
|
|
def draw_wrapped_text(draw, x, y, text, font, max_width, line_height, color):
|
|||
|
|
"""Рисует текст с автоматическим переносом"""
|
|||
|
|
lines = wrap_text(text, font, max_width)
|
|||
|
|
|
|||
|
|
for i, line in enumerate(lines):
|
|||
|
|
draw.text((x, y + i * line_height), line, font=font, fill=color)
|
|||
|
|
|
|||
|
|
return len(lines)
|
|||
|
|
|
|||
|
|
def generate_certificate_image(category, mascot, user_data):
|
|||
|
|
"""Генерация изображения сертификата"""
|
|||
|
|
# Проверяем существование шаблона
|
|||
|
|
template_path = TEMPLATES.get(mascot)
|
|||
|
|
|
|||
|
|
if not template_path or not os.path.exists(template_path):
|
|||
|
|
st.error(f"Шаблон для маскота '{mascot}' не найден!")
|
|||
|
|
st.info(f"Ожидаемый путь: {template_path}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Проверяем существование шрифта
|
|||
|
|
if not os.path.exists(FONT_PATH):
|
|||
|
|
st.error(f"Шрифт не найден по пути: {FONT_PATH}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# Загружаем шаблон
|
|||
|
|
img = Image.open(template_path)
|
|||
|
|
draw = ImageDraw.Draw(img)
|
|||
|
|
|
|||
|
|
# Загружаем шрифт OpenSans
|
|||
|
|
title_font = ImageFont.truetype(FONT_PATH, font_sizes["title"])
|
|||
|
|
content_font = ImageFont.truetype(FONT_PATH, font_sizes["content"])
|
|||
|
|
comment_font = ImageFont.truetype(FONT_PATH, font_sizes["comment"])
|
|||
|
|
signature_font = ImageFont.truetype(FONT_PATH, font_sizes["signature"])
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при загрузке ресурсов: {str(e)}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Выбираем случайный текст из пулов
|
|||
|
|
title = random.choice(title_pools[category])
|
|||
|
|
comment = random.choice(comment_pools[category])
|
|||
|
|
signature_text = random.choice(signature_pools[category])
|
|||
|
|
|
|||
|
|
# Формируем основной текст в зависимости от категории
|
|||
|
|
if category == "Новогоднее поздравление":
|
|||
|
|
main_text = f"Пожелания: {user_data.get('promise', '')}"
|
|||
|
|
else:
|
|||
|
|
main_text = f"Для анона: {user_data.get('username', '')}\nПричина вручения: {user_data.get('reason', '')}"
|
|||
|
|
|
|||
|
|
# Получаем параметры областей
|
|||
|
|
title_x, title_y, title_width, title_line_height = text_areas["title"]
|
|||
|
|
content_x, content_y, content_width, content_line_height = text_areas["content"]
|
|||
|
|
comment_x, comment_y, comment_width, comment_line_height = text_areas["comment"]
|
|||
|
|
signature_x, signature_y, signature_width, signature_line_height = text_areas["signature"]
|
|||
|
|
|
|||
|
|
# Рисуем текст на изображении с автоматическим переносом
|
|||
|
|
# Заголовок
|
|||
|
|
draw_wrapped_text(draw, title_x, title_y, title, title_font, title_width, title_line_height, FONT_COLOR)
|
|||
|
|
|
|||
|
|
# Основной текст
|
|||
|
|
draw_wrapped_text(draw, content_x, content_y, main_text, content_font, content_width, content_line_height, FONT_COLOR)
|
|||
|
|
|
|||
|
|
# Комментарий
|
|||
|
|
draw_wrapped_text(draw, comment_x, comment_y, comment, comment_font, comment_width, comment_line_height, FONT_COLOR)
|
|||
|
|
|
|||
|
|
# Подпись/нижний текст
|
|||
|
|
draw_wrapped_text(draw, signature_x, signature_y, signature_text, signature_font, signature_width, signature_line_height, FONT_COLOR)
|
|||
|
|
|
|||
|
|
# Сохранение временного файла
|
|||
|
|
temp_file = os.path.join(BASE_DIR, 'certificate.png')
|
|||
|
|
try:
|
|||
|
|
img.save(temp_file)
|
|||
|
|
return temp_file
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при сохранении сертификата: {str(e)}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Функция для загрузки изображения и конвертации в base64
|
|||
|
|
def get_image_base64(path):
|
|||
|
|
if os.path.exists(path):
|
|||
|
|
try:
|
|||
|
|
with open(path, "rb") as img_file:
|
|||
|
|
return base64.b64encode(img_file.read()).decode()
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при чтении файла: {str(e)}")
|
|||
|
|
return None
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Функция для обновления категории
|
|||
|
|
def update_category(new_category):
|
|||
|
|
"""Обновляет категорию и очищает соответствующие поля"""
|
|||
|
|
st.session_state.category = new_category
|
|||
|
|
if new_category == "Новогоднее поздравление":
|
|||
|
|
st.session_state.username = ''
|
|||
|
|
st.session_state.reason = ''
|
|||
|
|
else:
|
|||
|
|
st.session_state.promise = ''
|
|||
|
|
|
|||
|
|
# Настройка страницы
|
|||
|
|
st.set_page_config(
|
|||
|
|
page_title="Glintwine for ejchan",
|
|||
|
|
page_icon="🍷",
|
|||
|
|
layout="wide"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Кастомный CSS для оформления
|
|||
|
|
st.markdown("""
|
|||
|
|
<style>
|
|||
|
|
.header-container {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
padding: 10px 0;
|
|||
|
|
border-bottom: 1px solid #eee;
|
|||
|
|
}
|
|||
|
|
.logo-img {
|
|||
|
|
height: 60px;
|
|||
|
|
width: 30px;
|
|||
|
|
margin-right: 20px;
|
|||
|
|
object-fit: contain;
|
|||
|
|
}
|
|||
|
|
.service-title {
|
|||
|
|
font-size: 34px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #2E8B57;
|
|||
|
|
margin: 0;
|
|||
|
|
line-height: 1.2;
|
|||
|
|
}
|
|||
|
|
.service-subtitle {
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #666;
|
|||
|
|
font-style: italic;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
.footer-container {
|
|||
|
|
margin-top: 50px;
|
|||
|
|
padding: 20px 0;
|
|||
|
|
text-align: center;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
.footer-text {
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: white;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
font-style: italic;
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 10px;
|
|||
|
|
background-color: rgba(46, 139, 87, 0.1);
|
|||
|
|
border-radius: 5px;
|
|||
|
|
display: inline-block;
|
|||
|
|
}
|
|||
|
|
.main-title {
|
|||
|
|
margin-top: 30px;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
color: #ffffff;
|
|||
|
|
}
|
|||
|
|
.stButton > button {
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
.stButton > button:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|||
|
|
}
|
|||
|
|
.text-info {
|
|||
|
|
background-color: #f8f9fa;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
border-left: 3px solid #2E8B57;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
""", unsafe_allow_html=True)
|
|||
|
|
|
|||
|
|
# Шапка с логотипом и названием сервиса
|
|||
|
|
logo_path = None
|
|||
|
|
for ext in ['.svg', '.png', '.jpg', '.jpeg', '.webp']:
|
|||
|
|
test_path = os.path.join(BASE_DIR, f'logo{ext}')
|
|||
|
|
if os.path.exists(test_path):
|
|||
|
|
logo_path = test_path
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# Отображаем шапку
|
|||
|
|
st.markdown("""
|
|||
|
|
<div class="header-container">
|
|||
|
|
""", unsafe_allow_html=True)
|
|||
|
|
|
|||
|
|
col1, col2, col3 = st.columns([1, 5, 1])
|
|||
|
|
with col1:
|
|||
|
|
if logo_path:
|
|||
|
|
st.image(logo_path, width=60)
|
|||
|
|
else:
|
|||
|
|
st.markdown('<div style="font-size: 48px; margin-right: 20px;">🍷</div>', unsafe_allow_html=True)
|
|||
|
|
|
|||
|
|
with col2:
|
|||
|
|
st.markdown('<p class="service-title">Glintwine for ejchan</p>', unsafe_allow_html=True)
|
|||
|
|
st.markdown('<p class="service-subtitle">Drug for dreams does not exists, but you can drink some glintwine. Cause why not?</p>', unsafe_allow_html=True)
|
|||
|
|
|
|||
|
|
with col3:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
st.markdown("</div>", unsafe_allow_html=True)
|
|||
|
|
|
|||
|
|
# Проверка необходимых файлов
|
|||
|
|
missing_templates = check_templates()
|
|||
|
|
if missing_templates:
|
|||
|
|
st.warning("⚠️ Некоторые шаблоны не найдены:")
|
|||
|
|
for missing in missing_templates:
|
|||
|
|
st.write(f" - {missing}")
|
|||
|
|
|
|||
|
|
if not check_font():
|
|||
|
|
st.error(f"⚠️ Шрифт OpenSans не найден по пути: {FONT_PATH}")
|
|||
|
|
|
|||
|
|
# Основной заголовок
|
|||
|
|
st.markdown('<h1 class="main-title">Генератор сертификатов для ежеанонов 🦔</h1>', unsafe_allow_html=True)
|
|||
|
|
st.write("Создай уникальный сертификат для себя или другого анона!")
|
|||
|
|
|
|||
|
|
# Инициализируем session state для хранения данных формы
|
|||
|
|
if 'category' not in st.session_state:
|
|||
|
|
st.session_state.category = CATEGORIES[0]
|
|||
|
|
if 'promise' not in st.session_state:
|
|||
|
|
st.session_state.promise = ''
|
|||
|
|
if 'username' not in st.session_state:
|
|||
|
|
st.session_state.username = ''
|
|||
|
|
if 'reason' not in st.session_state:
|
|||
|
|
st.session_state.reason = ''
|
|||
|
|
if 'mascot' not in st.session_state:
|
|||
|
|
st.session_state.mascot = MASCOTS[0]
|
|||
|
|
|
|||
|
|
# Используем st.radio для выбора категории
|
|||
|
|
st.write("### Выбери категорию:")
|
|||
|
|
category = st.radio(
|
|||
|
|
"Категория",
|
|||
|
|
CATEGORIES,
|
|||
|
|
index=CATEGORIES.index(st.session_state.category),
|
|||
|
|
key="category_radio",
|
|||
|
|
label_visibility="collapsed",
|
|||
|
|
horizontal=True
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Обновляем session state при изменении категории
|
|||
|
|
if category != st.session_state.category:
|
|||
|
|
update_category(category)
|
|||
|
|
|
|||
|
|
# Показываем текущую выбранную категорию
|
|||
|
|
st.write(f"**Выбрана категория:** {st.session_state.category}")
|
|||
|
|
|
|||
|
|
# Используем форму с фиксированным ключом
|
|||
|
|
with st.form(key='certificate_form'):
|
|||
|
|
st.write("### Заполни данные:")
|
|||
|
|
|
|||
|
|
# Динамические поля в зависимости от категории
|
|||
|
|
if st.session_state.category == "Новогоднее поздравление":
|
|||
|
|
promise = st.text_area(
|
|||
|
|
"Твоё поздравление",
|
|||
|
|
value=st.session_state.promise,
|
|||
|
|
key='promise_input',
|
|||
|
|
height=100
|
|||
|
|
)
|
|||
|
|
st.session_state.promise = promise
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
username = st.text_input(
|
|||
|
|
"Вставь номер поста или имя анона, кому будет выдано",
|
|||
|
|
value=st.session_state.username,
|
|||
|
|
key='username_input'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
reason_label = "Причина молодца" if st.session_state.category == "Ебать ты молодец" else "Причина фейла"
|
|||
|
|
reason_example = "Охуенный пост, познавательный" if st.session_state.category == "Ебать ты молодец" else "'Обосрался в дискуссии о чае'"
|
|||
|
|
|
|||
|
|
reason = st.text_area(
|
|||
|
|
f"{reason_label} (например: {reason_example})",
|
|||
|
|
value=st.session_state.reason,
|
|||
|
|
key='reason_input',
|
|||
|
|
height=80,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
st.session_state.username = username
|
|||
|
|
st.session_state.reason = reason
|
|||
|
|
|
|||
|
|
# Выбор маскота
|
|||
|
|
mascot = st.selectbox(
|
|||
|
|
"Выберите оформление",
|
|||
|
|
MASCOTS,
|
|||
|
|
index=MASCOTS.index(st.session_state.mascot) if st.session_state.mascot in MASCOTS else 0,
|
|||
|
|
key='mascot_select'
|
|||
|
|
)
|
|||
|
|
st.session_state.mascot = mascot
|
|||
|
|
|
|||
|
|
# Дополнительные опции
|
|||
|
|
save_to_gallery = st.checkbox("Сохранить в галерею", key='gallery_checkbox')
|
|||
|
|
submit_button = st.form_submit_button(label='🎨 Сгенерировать сертификат')
|
|||
|
|
|
|||
|
|
if submit_button:
|
|||
|
|
# Подготавливаем user_data
|
|||
|
|
user_data = {}
|
|||
|
|
if st.session_state.category == "Новогоднее поздравление":
|
|||
|
|
user_data['promise'] = st.session_state.promise
|
|||
|
|
else:
|
|||
|
|
user_data['username'] = st.session_state.username
|
|||
|
|
user_data['reason'] = st.session_state.reason
|
|||
|
|
|
|||
|
|
# Проверка заполнения полей
|
|||
|
|
is_valid = True
|
|||
|
|
error_message = ""
|
|||
|
|
|
|||
|
|
if st.session_state.category == "Новогоднее поздравление":
|
|||
|
|
if not user_data.get('promise'):
|
|||
|
|
error_message = "Заполните поле 'Твоё поздравление'!"
|
|||
|
|
is_valid = False
|
|||
|
|
else:
|
|||
|
|
if not user_data.get('username'):
|
|||
|
|
error_message = "Заполните поле 'Номер поста или имя анона'!"
|
|||
|
|
is_valid = False
|
|||
|
|
elif not user_data.get('reason'):
|
|||
|
|
field_name = "Причина молодца" if st.session_state.category == "Ебать ты молодец" else "Причина фейла"
|
|||
|
|
error_message = f"Заполните поле '{field_name}'!"
|
|||
|
|
is_valid = False
|
|||
|
|
|
|||
|
|
if not is_valid:
|
|||
|
|
st.error(error_message)
|
|||
|
|
else:
|
|||
|
|
# Генерация изображения с индикатором загрузки
|
|||
|
|
with st.spinner("Генерируем сертификат..."):
|
|||
|
|
cert_path = generate_certificate_image(st.session_state.category, st.session_state.mascot, user_data)
|
|||
|
|
|
|||
|
|
if cert_path:
|
|||
|
|
# Отображение сертификата
|
|||
|
|
st.success("Сертификат готов!")
|
|||
|
|
st.image(cert_path, caption="Ура!")
|
|||
|
|
|
|||
|
|
# Кнопка скачивания
|
|||
|
|
try:
|
|||
|
|
with open(cert_path, "rb") as file:
|
|||
|
|
st.download_button(
|
|||
|
|
label="💾 Скачать сертификат",
|
|||
|
|
data=file,
|
|||
|
|
file_name="certificate.png",
|
|||
|
|
mime="image/png"
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при подготовке файла для скачивания: {str(e)}")
|
|||
|
|
|
|||
|
|
# Сохранение в галерею
|
|||
|
|
if save_to_gallery:
|
|||
|
|
try:
|
|||
|
|
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|||
|
|
unique_name = f"cert_{now}.png"
|
|||
|
|
gallery_path = os.path.join(GALLERY_DIR, unique_name)
|
|||
|
|
shutil.copy(cert_path, gallery_path)
|
|||
|
|
|
|||
|
|
# Сохраняем данные в CSV
|
|||
|
|
new_entry = pd.DataFrame([[str(user_data), st.session_state.category, st.session_state.mascot, now, gallery_path]],
|
|||
|
|
columns=['Resolution', 'Category', 'Mascot', 'Timestamp', 'ImagePath'])
|
|||
|
|
try:
|
|||
|
|
df = pd.read_csv(CSV_FILE)
|
|||
|
|
except (pd.errors.EmptyDataError, pd.errors.ParserError):
|
|||
|
|
initialize_csv()
|
|||
|
|
df = pd.read_csv(CSV_FILE)
|
|||
|
|
|
|||
|
|
df = pd.concat([df, new_entry], ignore_index=True)
|
|||
|
|
df.to_csv(CSV_FILE, index=False)
|
|||
|
|
|
|||
|
|
st.success("✅ Сертификат сохранён в галерее!")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при сохранении в галерею: {str(e)}")
|
|||
|
|
|
|||
|
|
# Галерея сертификатов
|
|||
|
|
st.header("Галерея сертификатов")
|
|||
|
|
st.caption("Последние сгенерированные сертификаты")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(CSV_FILE) and os.path.getsize(CSV_FILE) > 0:
|
|||
|
|
df = pd.read_csv(CSV_FILE)
|
|||
|
|
if not df.empty:
|
|||
|
|
# Получаем только существующие файлы
|
|||
|
|
valid_images = []
|
|||
|
|
for i, row in df.iterrows():
|
|||
|
|
if os.path.exists(row['ImagePath']):
|
|||
|
|
valid_images.append(row)
|
|||
|
|
|
|||
|
|
if valid_images:
|
|||
|
|
# Показываем последние 9 сертификатов (3 ряда по 3)
|
|||
|
|
recent_images = list(reversed(valid_images))[:9]
|
|||
|
|
|
|||
|
|
# Создаем сетку 3x3
|
|||
|
|
for i in range(0, len(recent_images), 3):
|
|||
|
|
cols = st.columns(3)
|
|||
|
|
for j in range(3):
|
|||
|
|
if i + j < len(recent_images):
|
|||
|
|
with cols[j]:
|
|||
|
|
row = recent_images[i + j]
|
|||
|
|
try:
|
|||
|
|
st.image(row['ImagePath'], caption=f"{row['Category']}")
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при загрузке изображения")
|
|||
|
|
else:
|
|||
|
|
st.write("Нет доступных изображений в галерее.")
|
|||
|
|
else:
|
|||
|
|
st.write("Пока пусто — будь первым!")
|
|||
|
|
else:
|
|||
|
|
st.write("Пока пусто — будь первым!")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
st.error(f"Ошибка при загрузке галереи: {str(e)}")
|
|||
|
|
st.write("Попробуйте сгенерировать новый сертификат.")
|
|||
|
|
|
|||
|
|
# Футер с надписью
|
|||
|
|
st.markdown("""
|
|||
|
|
<div class="footer-container">
|
|||
|
|
<div class="footer-text">
|
|||
|
|
Glintwine v1.1a. The d4d project
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
""", unsafe_allow_html=True)
|