/
TimTim
/
PrinterBOTAI
Обзор
Документация
Войти
/
TimTim
/
PrinterBOTAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
BEST3.py
1 866 строк
94 KB
TimTim
Create: BEST3.py, bot.py
21 июл 2026, 05:52
Верифицирован
21 июл 2026, 05:52
f600f02
Код
Авторство
О чём код?
import os import sys import csv import json import subprocess import threading import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, colorchooser, filedialog from pathlib import Path from datetime import datetime import re try: import psutil PSUTIL_AVAILABLE = True except ImportError: PSUTIL_AVAILABLE = False print("⚠️ psutil не установлен. Установите: pip install psutil") STATS_FILE = "stats.csv" PHOTOS_DIR = "photos" BOT_SCRIPT = "bot.py" CONFIG_FILE = "config.json" LOCK_FILE = "bot.lock" FLAG_FILE = "bot_running.flag" BLACKLIST_FILE = "blacklist.json" # ======================================================================== # УТИЛИТЫ ДЛЯ РАБОТЫ С ПРИНТЕРАМИ (ИСПРАВЛЕНО) # ======================================================================== def get_printers_windows(): """Получение списка принтеров в Windows""" try: import win32print printers = win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS) return [p[2] for p in printers] except ImportError: return [] except Exception: return [] def get_printers_linux(): """Получение списка принтеров в Linux/Mac""" try: import subprocess result = subprocess.run(['lpstat', '-e'], capture_output=True, text=True, timeout=5) return [p.strip() for p in result.stdout.split('\n') if p.strip()] except: return [] def get_printers(): """Универсальная функция получения списка принтеров""" if os.name == 'nt': printers = get_printers_windows() if printers: return printers # Если win32print не работает, пробуем через реестр try: import winreg key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows NT\CurrentVersion\PrinterPorts") printers = [] i = 0 while True: try: name = winreg.EnumValue(key, i)[0] if name and not name.startswith('_'): printers.append(name) i += 1 except WindowsError: break winreg.CloseKey(key) return printers except: return [] else: return get_printers_linux() # ======================================================================== # БОЛЬШОЙ НАБОР ЭМОДЗИ (ИЗ БИБЛИОТЕКИ) # ======================================================================== EMOJIS = [ # Смайлы и эмоции ("😊 Улыбка", "😊"), ("😂 Смех", "😂"), ("❤️ Сердце", "❤️"), ("👍 Класс", "👍"), ("🎉 Праздник", "🎉"), ("🔥 Огонь", "🔥"), ("⭐ Звезда", "⭐"), ("💪 Сила", "💪"), ("🤔 Думать", "🤔"), ("🙏 Спасибо", "🙏"), ("✨ Блеск", "✨"), ("🌟 Звезда", "🌟"), ("😍 Влюбленность", "😍"), ("🥰 Обожание", "🥰"), ("😘 Поцелуй", "😘"), ("😗 Свист", "😗"), ("😙 Целовать", "😙"), ("😚 Чмок", "😚"), ("🥲 Слеза", "🥲"), ("😄 Счастье", "😄"), ("😃 Радость", "😃"), ("😀 Улыбка", "😀"), ("😁 Сияние", "😁"), ("😆 Смех", "😆"), ("😅 Пот", "😅"), ("🤣 Ржание", "🤣"), ("☺️ Теплота", "☺️"), ("😊 Улыбка", "😊"), ("😇 Ангел", "😇"), ("🙂 Легкая улыбка", "🙂"), ("🙃 Перевертыш", "🙃"), ("😉 Подмигивание", "😉"), ("😌 Облегчение", "😌"), ("😍 Влюбленность", "😍"), ("🥰 Обожание", "🥰"), ("😘 Поцелуй", "😘"), ("😗 Свист", "😗"), # Жесты ("👋 Привет", "👋"), ("🤝 Рукопожатие", "🤝"), ("✌️ Мир", "✌️"), ("🤘 Рок", "🤘"), ("👌 Окей", "👌"), ("✊ Кулак", "✊"), ("🖐 Ладонь", "🖐"), ("💪 Бицепс", "💪"), ("🙌 Ура", "🙌"), ("👏 Аплодисменты", "👏"), ("🤲 Ладони", "🤲"), ("🤟 Люблю", "🤟"), ("🤙 Позвони", "🤙"), ("👈 Указатель", "👈"), ("👉 Указатель", "👉"), ("👆 Вверх", "👆"), ("👇 Вниз", "👇"), ("☝️ Вверх", "☝️"), ("👍 Класс", "👍"), ("👎 Не класс", "👎"), # Предметы ("📸 Фото", "📸"), ("🖨 Печать", "🖨"), ("✅ Да", "✅"), ("❌ Нет", "❌"), ("📋 Список", "📋"), ("🔍 Поиск", "🔍"), ("⚙️ Настройки", "⚙️"), ("💡 Идея", "💡"), ("⚠️ Внимание", "⚠️"), ("🚫 Запрет", "🚫"), ("📌 Булавка", "📌"), ("💬 Диалог", "💬"), ("📝 Заметка", "📝"), ("📎 Скрепка", "📎"), ("✏️ Карандаш", "✏️"), ("🖊 Ручка", "🖊"), ("🖌 Кисть", "🖌"), ("🖍 Карандаш", "🖍"), ("📁 Папка", "📁"), ("📂 Открыть", "📂"), ("📅 Календарь", "📅"), ("📆 Отрывной", "📆"), ("📊 График", "📊"), ("📈 Рост", "📈"), ("📉 Падение", "📉"), ("📋 Список", "📋"), ("📌 Булавка", "📌"), ("📍 Кнопка", "📍"), # Животные ("🐱 Кот", "🐱"), ("🐶 Собака", "🐶"), ("🐭 Мышь", "🐭"), ("🐹 Хомяк", "🐹"), ("🐰 Кролик", "🐰"), ("🦊 Лиса", "🦊"), ("🐻 Медведь", "🐻"), ("🐼 Панда", "🐼"), ("🐨 Коала", "🐨"), ("🐯 Тигр", "🐯"), ("🦁 Лев", "🦁"), ("🐮 Корова", "🐮"), ("🐷 Свинья", "🐷"), ("🐸 Лягушка", "🐸"), ("🐵 Обезьяна", "🐵"), ("🐔 Курица", "🐔"), ("🐧 Пингвин", "🐧"), ("🐦 Птица", "🐦"), ("🐤 Цыпленок", "🐤"), ("🦆 Утка", "🦆"), ("🦉 Сова", "🦉"), ("🦇 Летучая мышь", "🦇"), ("🐺 Волк", "🐺"), ("🐗 Кабан", "🐗"), ("🐴 Лошадь", "🐴"), ("🦄 Единорог", "🦄"), ("🐝 Пчела", "🐝"), ("🐛 Гусеница", "🐛"), ("🦋 Бабочка", "🦋"), ("🐌 Улитка", "🐌"), ("🐞 Божья коровка", "🐞"), ("🐜 Муравей", "🐜"), # Еда ("🍎 Яблоко", "🍎"), ("🍐 Груша", "🍐"), ("🍊 Апельсин", "🍊"), ("🍋 Лимон", "🍋"), ("🍌 Банан", "🍌"), ("🍉 Арбуз", "🍉"), ("🍇 Виноград", "🍇"), ("🍓 Клубника", "🍓"), ("🍑 Персик", "🍑"), ("🍒 Вишня", "🍒"), ("🍍 Ананас", "🍍"), ("🥝 Киви", "🥝"), ("🥑 Авокадо", "🥑"), ("🍆 Баклажан", "🍆"), ("🥔 Картофель", "🥔"), ("🥕 Морковь", "🥕"), ("🌽 Кукуруза", "🌽"), ("🥦 Брокколи", "🥦"), ("🥬 Салат", "🥬"), ("🥒 Огурец", "🥒"), ("🌶 Перец", "🌶"), ("🫑 Болгарский перец", "🫑"), ("🥜 Арахис", "🥜"), ("🌰 Каштан", "🌰"), ("🍞 Хлеб", "🍞"), ("🥐 Круассан", "🥐"), ("🥖 Багет", "🥖"), ("🥨 Крендель", "🥨"), ("🥯 Бублик", "🥯"), ("🥞 Блины", "🥞"), ("🧇 Вафля", "🧇"), ("🧀 Сыр", "🧀"), ("🍖 Мясо", "🍖"), ("🍗 Бедро", "🍗"), ("🥩 Стейк", "🥩"), ("🥓 Бекон", "🥓"), ("🍔 Бургер", "🍔"), ("🍟 Фри", "🍟"), ("🍕 Пицца", "🍕"), ("🌭 Хот-дог", "🌭"), ("🥪 Сэндвич", "🥪"), ("🌮 Тако", "🌮"), ("🌯 Буррито", "🌯"), ("🥙 Фалафель", "🥙"), ("🧆 Фалафель", "🧆"), ("🥚 Яйцо", "🥚"), ("🍳 Яичница", "🍳"), ("🥘 Сковорода", "🥘"), ("🍲 Суп", "🍲"), ("🥣 Миска", "🥣"), ("🥗 Салат", "🥗"), ("🧈 Масло", "🧈"), ("🧂 Соль", "🧂"), ("🥫 Консервы", "🥫"), ("🍝 Паста", "🍝"), ("🍜 Лапша", "🍜"), ("🍛 Карри", "🍛"), ("🍙 Рис", "🍙"), ("🍚 Вареный рис", "🍚"), ("🍘 Крекер", "🍘"), ("🍥 Рыбный пирог", "🍥"), ("🥮 Лунный пирог", "🥮"), ("🍢 Оден", "🍢"), ("🍡 Данго", "🍡"), # Напитки ("🍺 Пиво", "🍺"), ("🍻 Кружки", "🍻"), ("🥂 Бокалы", "🥂"), ("🥃 Виски", "🥃"), ("🍷 Вино", "🍷"), ("🍸 Коктейль", "🍸"), ("🍹 Сок", "🍹"), ("🍾 Шампанское", "🍾"), ("🧊 Лед", "🧊"), ("🥤 Стакан", "🥤"), ("🧃 Сок", "🧃"), ("🧉 Мате", "🧉"), ("☕ Кофе", "☕"), ("🫖 Чайник", "🫖"), ("🍵 Чай", "🍵"), ("🧋 Боба", "🧋"), # Транспорт ("🚗 Авто", "🚗"), ("🚕 Такси", "🚕"), ("🚙 Внедорожник", "🚙"), ("🚌 Автобус", "🚌"), ("🚎 Троллейбус", "🚎"), ("🏎 Гоночный", "🏎"), ("🚓 Полиция", "🚓"), ("🚑 Скорая", "🚑"), ("🚒 Пожарная", "🚒"), ("🚐 Микроавтобус", "🚐"), ("🚚 Грузовик", "🚚"), ("🚛 Фура", "🚛"), ("🚜 Трактор", "🚜"), ("🏍 Мотоцикл", "🏍"), ("🛵 Скутер", "🛵"), ("🚲 Велосипед", "🚲"), ("🛴 Самокат", "🛴"), ("🛹 Скейтборд", "🛹"), ("🛼 Ролики", "🛼"), ("🚁 Вертолет", "🚁"), ("✈️ Самолет", "✈️"), ("🛩 Легкий", "🛩"), ("🛫 Взлет", "🛫"), ("🛬 Посадка", "🛬"), ("🪂 Парашют", "🪂"), ("💺 Кресло", "💺"), ("🚀 Ракета", "🚀"), ("🛸 НЛО", "🛸"), ("🚢 Корабль", "🚢"), ("🛳 Пассажирский", "🛳"), ("⛴ Паром", "⛴"), ("🚤 Катер", "🚤"), ("🛥 Моторная", "🛥"), ("⛵ Парусник", "⛵"), ("🛶 Каноэ", "🛶"), ("🚣 Гребля", "🚣"), ("🏊 Плавание", "🏊"), ("⛹️♂️ Баскетбол", "⛹️"), ("🏋️♂️ Тяжелая атлетика", "🏋️"), ("🚴 Велоспорт", "🚴"), ("🚵 Маунтинбайк", "🚵"), ("🤸 Гимнастика", "🤸"), ("🤼 Борьба", "🤼"), ("🤽 Водное поло", "🤽"), ("🤾 Гандбол", "🤾"), ("🤺 Фехтование", "🤺"), ("⛷ Лыжи", "⛷"), ("🏂 Сноуборд", "🏂"), ("🏌 Гольф", "🏌"), ("🏇 Скачки", "🏇"), ("🧗 Скалолазание", "🧗"), ("🧘 Йога", "🧘"), ("🪂 Парашют", "🪂"), # Природа ("🌍 Земля", "🌍"), ("🌎 Земля", "🌎"), ("🌏 Земля", "🌏"), ("🌐 Глобус", "🌐"), ("🗺 Карта", "🗺"), ("🗾 Япония", "🗾"), ("🧭 Компас", "🧭"), ("🏔 Горы", "🏔"), ("⛰ Скалы", "⛰"), ("🌋 Вулкан", "🌋"), ("🗻 Фудзи", "🗻"), ("🏕 Кемпинг", "🏕"), ("🏖 Пляж", "🏖"), ("🏜 Пустыня", "🏜"), ("🏝 Остров", "🏝"), ("🏞 Национальный парк", "🏞"), ("🏟 Стадион", "🏟"), ("🏛 Здание", "🏛"), ("🏗 Стройка", "🏗"), ("🧱 Кирпич", "🧱"), ("🪨 Камень", "🪨"), ("🪵 Бревно", "🪵"), ("🪹 Гнездо", "🪹"), ("🪺 Гнездо с яйцами", "🪺"), ("🌲 Ель", "🌲"), ("🌳 Дуб", "🌳"), ("🌴 Пальма", "🌴"), ("🌵 Кактус", "🌵"), ("🌾 Рис", "🌾"), ("🌿 Трава", "🌿"), ("☘ Трилистник", "☘"), ("🍀 Клевер", "🍀"), ("🍁 Клен", "🍁"), ("🍂 Осенние", "🍂"), ("🍃 Ветер", "🍃"), ("🌺 Цветок", "🌺"), ("🌸 Сакура", "🌸"), ("🌷 Тюльпан", "🌷"), ("🌹 Роза", "🌹"), ("🥀 Увядшая", "🥀"), ("🌻 Подсолнух", "🌻"), ("🌼 Ромашка", "🌼"), ("💐 Букет", "💐"), ("🌱 Росток", "🌱"), # Погода ("☀️ Солнце", "☀️"), ("🌤 Солнце", "🌤"), ("⛅ Облачно", "⛅"), ("🌥 Облака", "🌥"), ("☁️ Облако", "☁️"), ("🌦 Дождь", "🌦"), ("🌧 Дождь", "🌧"), ("⛈ Гроза", "⛈"), ("🌩 Молния", "🌩"), ("🌨 Снег", "🌨"), ("❄️ Снежинка", "❄️"), ("☃️ Снеговик", "☃️"), ("⛄ Снеговик", "⛄"), ("🌫 Туман", "🌫"), ("💨 Ветер", "💨"), ("🌈 Радуга", "🌈"), ("☔ Дождь", "☔"), ("☂️ Зонт", "☂️"), ("☃️ Снеговик", "☃️"), ("⛄ Снеговик", "⛄"), ] # ======================================================================== # КРАСИВЫЙ РЕДАКТОР ТЕКСТА (ИСПРАВЛЕН) # ======================================================================== class ModernTextEditor: def __init__(self, parent, title, text, callback, is_multiline=True): self.parent = parent self.callback = callback self.is_multiline = is_multiline self.original_text = text if text else "" self.dialog = tk.Toplevel(parent) self.dialog.title(f"✏️ {title}") self.dialog.geometry("950x700") self.dialog.minsize(750, 500) self.dialog.transient(parent) self.dialog.grab_set() self.dialog.configure(bg='#f0f2f5') # Основной контейнер с весом для адаптации main = tk.Frame(self.dialog, bg='#f0f2f5') main.pack(fill=tk.BOTH, expand=True, padx=20, pady=15) # Заголовок tk.Label(main, text="✏️ Редактор текста", font=('Segoe UI', 16, 'bold'), bg='#f0f2f5', fg='#1a1a2e').pack(anchor=tk.W, pady=(0, 10)) # Панель инструментов toolbar = tk.Frame(main, bg='#f0f2f5') toolbar.pack(fill=tk.X, pady=(0, 10)) btn_style = {'font': ('Segoe UI', 9), 'bg': '#ffffff', 'relief': tk.FLAT, 'padx': 12, 'pady': 4, 'cursor': 'hand2', 'bd': 1} tk.Button(toolbar, text="😊 Эмодзи", command=self.show_emoji_menu, **btn_style).pack(side=tk.LEFT, padx=2) tk.Button(toolbar, text="📋 Вставить", command=self.paste, **btn_style).pack(side=tk.LEFT, padx=2) tk.Button(toolbar, text="↺ Отменить", command=self.undo, **btn_style).pack(side=tk.LEFT, padx=2) tk.Button(toolbar, text="↻ Повторить", command=self.redo, **btn_style).pack(side=tk.LEFT, padx=2) tk.Button(toolbar, text="🧹 Очистить", command=self.clear_text, **btn_style).pack(side=tk.LEFT, padx=2) self.char_count = tk.Label(toolbar, text="Символов: 0", font=('Segoe UI', 9), bg='#f0f2f5', fg='#6b7280') self.char_count.pack(side=tk.RIGHT, padx=5) # Основное поле (разделитель) с весом paned = tk.PanedWindow(main, bg='#f0f2f5', sashwidth=5, sashrelief=tk.RAISED) paned.pack(fill=tk.BOTH, expand=True) # Левая панель – редактор left = tk.Frame(paned, bg='#ffffff', relief=tk.FLAT, bd=0) paned.add(left, width=400) tk.Label(left, text="📝 Редактируемый текст", font=('Segoe UI', 11, 'bold'), bg='#ffffff', fg='#1a1a2e').pack(anchor=tk.W, padx=15, pady=(10, 5)) editor_frame = tk.Frame(left, bg='#ffffff') editor_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) if self.is_multiline: self.text_widget = tk.Text(editor_frame, font=('Segoe UI', 11), wrap=tk.WORD, bg='#fafafa', fg='#1a1a2e', insertbackground='#4a90d9', selectbackground='#4a90d930', relief=tk.FLAT, borderwidth=1, highlightthickness=2, highlightcolor='#4a90d9', highlightbackground='#d1d5db', padx=10, pady=10) if self.original_text: self.text_widget.insert("1.0", self.original_text) self.text_widget.pack(fill=tk.BOTH, expand=True) else: self.text_widget = tk.Entry(editor_frame, font=('Segoe UI', 11), bg='#fafafa', fg='#1a1a2e', relief=tk.FLAT, borderwidth=1, highlightthickness=2, highlightcolor='#4a90d9', highlightbackground='#d1d5db') if self.original_text: self.text_widget.insert(0, self.original_text) self.text_widget.pack(fill=tk.X, pady=10) # Правая панель – предпросмотр right = tk.Frame(paned, bg='#ffffff', relief=tk.FLAT, bd=0) paned.add(right, width=400) tk.Label(right, text="👁 Предпросмотр в Telegram", font=('Segoe UI', 11, 'bold'), bg='#ffffff', fg='#1a1a2e').pack(anchor=tk.W, padx=15, pady=(10, 5)) preview_frame = tk.Frame(right, bg='#e8edf0') preview_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) self.preview_canvas = tk.Canvas(preview_frame, bg='#e8edf0', relief=tk.FLAT, borderwidth=0, highlightthickness=0) self.preview_canvas.pack(fill=tk.BOTH, expand=True) # Привязываем обновление при изменении размера self.preview_canvas.bind('<Configure>', lambda e: self.update_preview()) self.text_widget.bind('<KeyRelease>', self.update_preview) # Инициализируем предпросмотр self.dialog.after(100, self.update_preview) # Кнопки внизу btn_frame = tk.Frame(main, bg='#f0f2f5') btn_frame.pack(fill=tk.X, pady=(15, 0)) btn_primary = {'font': ('Segoe UI', 10), 'bg': '#4a90d9', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 25, 'pady': 8, 'cursor': 'hand2', 'bd': 0} btn_secondary = {'font': ('Segoe UI', 10), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 25, 'pady': 8, 'cursor': 'hand2', 'bd': 0} left_btns = tk.Frame(btn_frame, bg='#f0f2f5') left_btns.pack(side=tk.LEFT) tk.Button(left_btns, text="✅ Сохранить", command=self.save, **btn_primary).pack(side=tk.LEFT, padx=3) tk.Button(left_btns, text="🔄 Сбросить", command=self.reset, **btn_secondary).pack(side=tk.LEFT, padx=3) tk.Button(left_btns, text="📋 Вставить", command=self.paste, **btn_secondary).pack(side=tk.LEFT, padx=3) right_btns = tk.Frame(btn_frame, bg='#f0f2f5') right_btns.pack(side=tk.RIGHT) tk.Button(right_btns, text="❌ Отмена", command=self.dialog.destroy, **btn_secondary).pack(side=tk.RIGHT, padx=3) # Горячие клавиши self.text_widget.focus_set() self.dialog.bind('<Control-z>', lambda e: self.undo()) self.dialog.bind('<Control-y>', lambda e: self.redo()) self.dialog.bind('<Control-s>', lambda e: self.save()) self.dialog.bind('<Escape>', lambda e: self.dialog.destroy()) # Центрируем окно self.center_window() def center_window(self): self.dialog.update_idletasks() w = self.dialog.winfo_width() h = self.dialog.winfo_height() x = (self.dialog.winfo_screenwidth() // 2) - (w // 2) y = (self.dialog.winfo_screenheight() // 2) - (h // 2) self.dialog.geometry(f'{w}x{h}+{x}+{y}') def get_text(self): if self.is_multiline: return self.text_widget.get("1.0", tk.END).strip() return self.text_widget.get().strip() def update_preview(self, event=None): """Обновляет предпросмотр в стиле Telegram""" self.preview_canvas.delete("all") width = max(200, self.preview_canvas.winfo_width() - 20) text = self.get_text() if not text: text = "📝 Введите текст..." lines = text.split('\n') line_height = 28 text_height = max(50, len(lines) * line_height + 20) padding = 15 x1, y1 = 15, 10 x2, y2 = width - 15, text_height + 15 radius = 12 # Тень self.preview_canvas.create_rectangle(x1+2, y1+4, x2+2, y2+4, fill='#00000015', outline='') # Скругленный прямоугольник (облако сообщения) self.preview_canvas.create_oval(x1, y1, x1 + radius*2, y1 + radius*2, fill='#ffffff', outline='') self.preview_canvas.create_oval(x2 - radius*2, y1, x2, y1 + radius*2, fill='#ffffff', outline='') self.preview_canvas.create_oval(x1, y2 - radius*2, x1 + radius*2, y2, fill='#ffffff', outline='') self.preview_canvas.create_oval(x2 - radius*2, y2 - radius*2, x2, y2, fill='#ffffff', outline='') self.preview_canvas.create_rectangle(x1 + radius, y1, x2 - radius, y2, fill='#ffffff', outline='') self.preview_canvas.create_rectangle(x1, y1 + radius, x2, y2 - radius, fill='#ffffff', outline='') # Текст self.preview_canvas.create_text( x1 + padding, y1 + padding, text=text, font=('Segoe UI', 11), anchor=tk.NW, width=width - padding*2 - 20, fill='#1a1a2e', justify=tk.LEFT ) # Время time_text = datetime.now().strftime("%H:%M") self.preview_canvas.create_text( x2 - 40, y2 - 8, text=time_text, font=('Segoe UI', 8), anchor=tk.SE, fill='#999' ) # Галочки (прочитано) self.preview_canvas.create_text( x2 - 8, y2 - 8, text="✓✓", font=('Segoe UI', 8), anchor=tk.SE, fill='#4fc3f7' ) self.preview_canvas.config(scrollregion=(0, 0, width, text_height + 30)) self.char_count.config(text=f"Символов: {len(self.get_text())}") def save(self): self.callback(self.get_text()) self.dialog.destroy() def reset(self): if self.is_multiline: self.text_widget.delete("1.0", tk.END) if self.original_text: self.text_widget.insert("1.0", self.original_text) else: self.text_widget.delete(0, tk.END) if self.original_text: self.text_widget.insert(0, self.original_text) self.update_preview() def clear_text(self): if self.is_multiline: self.text_widget.delete("1.0", tk.END) else: self.text_widget.delete(0, tk.END) self.update_preview() def paste(self): try: clip = self.dialog.clipboard_get() self.text_widget.insert(tk.INSERT, clip) self.update_preview() except: pass def undo(self): if self.is_multiline: self.text_widget.event_generate("<<Undo>>") self.update_preview() def redo(self): if self.is_multiline: self.text_widget.event_generate("<<Redo>>") self.update_preview() def show_emoji_menu(self): """Показывает меню с эмодзи из библиотеки""" menu = tk.Menu(self.dialog, tearoff=0) # Группируем эмодзи по категориям categories = { "😊 Смайлы": EMOJIS[:20], "👋 Жесты": EMOJIS[20:35], "📸 Предметы": EMOJIS[35:55], "🐱 Животные": EMOJIS[55:80], "🍎 Еда": EMOJIS[80:120], "🍺 Напитки": EMOJIS[120:135], "🚗 Транспорт": EMOJIS[135:175], "🌍 Природа": EMOJIS[175:210], "☀️ Погода": EMOJIS[210:230], } for category, emojis in categories.items(): submenu = tk.Menu(menu, tearoff=0) for label, emoji in emojis: submenu.add_command(label=label, command=lambda e=emoji: self.insert_emoji(e)) menu.add_cascade(label=category, menu=submenu) try: menu.tk_popup(self.dialog.winfo_pointerx(), self.dialog.winfo_pointery()) finally: menu.grab_release() def insert_emoji(self, emoji): self.text_widget.insert(tk.INSERT, emoji) self.update_preview() # ======================================================================== # ОКНО ВЫБОРА ПРИНТЕРА (ИСПРАВЛЕНО) # ======================================================================== class PrinterSelector: def __init__(self, parent, current_printer, callback): self.callback = callback self.current_printer = current_printer self.dialog = tk.Toplevel(parent) self.dialog.title("🖨 Выбор принтера") self.dialog.geometry("550x450") self.dialog.minsize(450, 350) self.dialog.transient(parent) self.dialog.grab_set() self.dialog.configure(bg='#f0f2f5') main = tk.Frame(self.dialog, bg='#f0f2f5') main.pack(fill=tk.BOTH, expand=True, padx=20, pady=15) tk.Label(main, text="🖨 Выберите принтер", font=('Segoe UI', 16, 'bold'), bg='#f0f2f5', fg='#1a1a2e').pack(anchor=tk.W, pady=(0, 10)) # Инфо о текущем info = tk.Frame(main, bg='#ffffff', relief=tk.FLAT, bd=1) info.pack(fill=tk.X, pady=(0, 10)) tk.Label(info, text="Текущий принтер:", font=('Segoe UI', 10), bg='#ffffff', fg='#6b7280').pack(side=tk.LEFT, padx=15, pady=10) self.current_label = tk.Label(info, text=current_printer if current_printer else "по умолчанию", font=('Segoe UI', 10, 'bold'), bg='#ffffff', fg='#4a90d9') self.current_label.pack(side=tk.LEFT, padx=5, pady=10) # Список list_frame = tk.Frame(main, bg='#ffffff', relief=tk.FLAT, bd=1) list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) scrollbar = tk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, font=('Segoe UI', 11), bg='#ffffff', fg='#1a1a2e', selectbackground='#4a90d930', selectforeground='#1a1a2e', relief=tk.FLAT, borderwidth=0, activestyle='none') self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.listbox.yview) # Загружаем список принтеров self.load_printers() # Кнопки btn_frame = tk.Frame(main, bg='#f0f2f5') btn_frame.pack(fill=tk.X, pady=(10, 0)) btn_primary = {'font': ('Segoe UI', 10), 'bg': '#4a90d9', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 25, 'pady': 8, 'cursor': 'hand2', 'bd': 0} btn_secondary = {'font': ('Segoe UI', 10), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 25, 'pady': 8, 'cursor': 'hand2', 'bd': 0} left = tk.Frame(btn_frame, bg='#f0f2f5') left.pack(side=tk.LEFT) tk.Button(left, text="✅ Выбрать", command=self.select, **btn_primary).pack(side=tk.LEFT, padx=3) tk.Button(left, text="🔄 Обновить", command=self.load_printers, **btn_secondary).pack(side=tk.LEFT, padx=3) right = tk.Frame(btn_frame, bg='#f0f2f5') right.pack(side=tk.RIGHT) tk.Button(right, text="❌ Отмена", command=self.dialog.destroy, **btn_secondary).pack(side=tk.RIGHT, padx=3) self.listbox.bind('<Double-Button-1>', lambda e: self.select()) self.dialog.bind('<Escape>', lambda e: self.dialog.destroy()) self.center_window() self.listbox.focus_set() def center_window(self): self.dialog.update_idletasks() w = self.dialog.winfo_width() h = self.dialog.winfo_height() x = (self.dialog.winfo_screenwidth() // 2) - (w // 2) y = (self.dialog.winfo_screenheight() // 2) - (h // 2) self.dialog.geometry(f'{w}x{h}+{x}+{y}') def load_printers(self): """Загружает список принтеров""" self.listbox.delete(0, tk.END) printers = get_printers() if not printers: self.listbox.insert(tk.END, "❌ Принтеры не найдены") self.listbox.insert(tk.END, "💡 Проверьте подключение принтера") return self.listbox.insert(0, "📌 Принтер по умолчанию") for p in printers: self.listbox.insert(tk.END, p) # Выделяем текущий принтер for i in range(self.listbox.size()): item = self.listbox.get(i) if item == self.current_printer: self.listbox.selection_set(i) self.listbox.see(i) break elif not self.current_printer and i == 0: self.listbox.selection_set(i) self.listbox.see(i) break def select(self): sel = self.listbox.curselection() if not sel: messagebox.showwarning("Предупреждение", "Выберите принтер из списка") return printer = self.listbox.get(sel[0]) if printer.startswith("❌"): return if printer == "📌 Принтер по умолчанию": printer = "" self.callback(printer) self.dialog.destroy() # ======================================================================== # ЧЕРНЫЙ СПИСОК # ======================================================================== class BlacklistManager: def __init__(self, parent, callback): self.parent = parent self.callback = callback self.blacklist = self.load_blacklist() def load_blacklist(self): if os.path.isfile(BLACKLIST_FILE): try: with open(BLACKLIST_FILE, "r", encoding="utf-8") as f: return json.load(f) except: pass return {"users": [], "reason": {}} def save_blacklist(self): try: with open(BLACKLIST_FILE, "w", encoding="utf-8") as f: json.dump(self.blacklist, f, indent=2, ensure_ascii=False) if self.callback: self.callback() return True except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить черный список: {e}") return False def show_dialog(self): dialog = tk.Toplevel(self.parent) dialog.title("🚫 Управление черным списком") dialog.geometry("700x550") dialog.minsize(600, 400) dialog.transient(self.parent) dialog.grab_set() dialog.configure(bg='#f0f2f5') main = tk.Frame(dialog, bg='#f0f2f5') main.pack(fill=tk.BOTH, expand=True, padx=20, pady=15) tk.Label(main, text="🚫 Управление черным списком", font=('Segoe UI', 16, 'bold'), bg='#f0f2f5', fg='#1a1a2e').pack(anchor=tk.W, pady=(0, 15)) # Добавление пользователя add_frame = tk.LabelFrame(main, text="➕ Добавить пользователя", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) add_frame.pack(fill=tk.X, pady=5) grid = tk.Frame(add_frame, bg='#ffffff') grid.pack(fill=tk.X) tk.Label(grid, text="ID пользователя:", bg='#ffffff', font=('Segoe UI', 10)).grid(row=0, column=0, padx=5, sticky=tk.W) self.user_id_entry = tk.Entry(grid, width=20, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) self.user_id_entry.grid(row=0, column=1, padx=5, sticky=tk.W) tk.Label(grid, text="Причина:", bg='#ffffff', font=('Segoe UI', 10)).grid(row=0, column=2, padx=5, sticky=tk.W) self.reason_entry = tk.Entry(grid, width=30, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) self.reason_entry.grid(row=0, column=3, padx=5, sticky=tk.W) btn_add = {'font': ('Segoe UI', 10), 'bg': '#4a90d9', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 15, 'pady': 5, 'cursor': 'hand2', 'bd': 0} tk.Button(grid, text="➕ Добавить", command=self.add_user, **btn_add).grid(row=0, column=4, padx=5) # Список list_frame = tk.LabelFrame(main, text="🚫 Заблокированные пользователи", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) list_frame.pack(fill=tk.BOTH, expand=True, pady=5) table_frame = tk.Frame(list_frame, bg='#ffffff') table_frame.pack(fill=tk.BOTH, expand=True) scroll_y = tk.Scrollbar(table_frame) scroll_y.pack(side=tk.RIGHT, fill=tk.Y) self.tree = ttk.Treeview(table_frame, columns=("id", "reason"), show="headings", yscrollcommand=scroll_y.set, height=10) scroll_y.config(command=self.tree.yview) self.tree.heading("id", text="🆔 ID") self.tree.heading("reason", text="📝 Причина") self.tree.column("id", width=150) self.tree.column("reason", width=350) self.tree.pack(fill=tk.BOTH, expand=True) btn_frame = tk.Frame(list_frame, bg='#ffffff') btn_frame.pack(fill=tk.X, pady=5) btn_sec = {'font': ('Segoe UI', 10), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 15, 'pady': 5, 'cursor': 'hand2', 'bd': 0} tk.Button(btn_frame, text="🗑 Удалить выбранного", command=self.remove_selected, **btn_sec).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="🔄 Обновить", command=self.refresh_list, **btn_sec).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="🚫 Очистить всё", command=self.clear_all, **btn_sec).pack(side=tk.LEFT, padx=2) self.info_label = tk.Label(main, text=f"📊 Всего заблокировано: {len(self.blacklist['users'])}", bg='#f0f2f5', fg='#1a1a2e', font=('Segoe UI', 10, 'bold')) self.info_label.pack(anchor=tk.W, pady=5) self.refresh_list() dialog.bind('<Escape>', lambda e: dialog.destroy()) self.center_window(dialog) def center_window(self, dialog): dialog.update_idletasks() w = dialog.winfo_width() h = dialog.winfo_height() x = (dialog.winfo_screenwidth() // 2) - (w // 2) y = (dialog.winfo_screenheight() // 2) - (h // 2) dialog.geometry(f'{w}x{h}+{x}+{y}') def refresh_list(self): for item in self.tree.get_children(): self.tree.delete(item) for uid in self.blacklist['users']: reason = self.blacklist['reason'].get(uid, "Не указана") self.tree.insert("", tk.END, values=(uid, reason)) self.info_label.config(text=f"📊 Всего заблокировано: {len(self.blacklist['users'])}") def add_user(self): uid = self.user_id_entry.get().strip() reason = self.reason_entry.get().strip() or "Не указана" if not uid: messagebox.showwarning("Предупреждение", "Введите ID пользователя") return try: int(uid) except: messagebox.showwarning("Предупреждение", "ID должен быть числом") return if uid in self.blacklist['users']: messagebox.showinfo("Информация", "Пользователь уже в черном списке") return self.blacklist['users'].append(uid) self.blacklist['reason'][uid] = reason self.save_blacklist() self.refresh_list() self.user_id_entry.delete(0, tk.END) self.reason_entry.delete(0, tk.END) messagebox.showinfo("Успех", f"Пользователь {uid} добавлен") def remove_selected(self): sel = self.tree.selection() if not sel: messagebox.showwarning("Предупреждение", "Выберите пользователя") return uid = self.tree.item(sel[0])['values'][0] if messagebox.askyesno("Подтверждение", f"Удалить пользователя {uid}?"): if uid in self.blacklist['users']: self.blacklist['users'].remove(uid) if uid in self.blacklist['reason']: del self.blacklist['reason'][uid] self.save_blacklist() self.refresh_list() messagebox.showinfo("Успех", f"Пользователь {uid} удален") def clear_all(self): if messagebox.askyesno("Предупреждение", "Очистить весь черный список?"): self.blacklist = {"users": [], "reason": {}} self.save_blacklist() self.refresh_list() messagebox.showinfo("Успех", "Черный список очищен") # ======================================================================== # ОСНОВНОЕ ПРИЛОЖЕНИЕ (ЕДИНЫЙ СТИЛЬ) # ======================================================================== class BotControlApp: def __init__(self, root): self.root = root self.root.title("🖨 Print Bot Admin Panel") self.root.geometry("1200x800") self.root.minsize(1000, 700) self.root.configure(bg='#f0f2f5') self.bot_process = None self.running = False self.stop_log_thread = False self.config = self.load_config() self.total_photos_printed = 0 self.session_photos_printed = 0 self.all_log_lines = [] self.auto_refresh = True self.filter_vars = {} self.setup_styles() self.create_widgets() self.load_total_counter() self.update_all_stats() self.check_existing_bot() self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.setup_hotkeys() self.start_auto_refresh() def setup_hotkeys(self): self.root.bind('<Control-s>', lambda e: self.save_all_texts()) self.root.bind('<F5>', lambda e: self.refresh_all()) self.root.bind('<Control-f>', lambda e: self.focus_search()) def setup_styles(self): style = ttk.Style() try: style.theme_use('clam') except: pass # Единые стили для всего приложения style.configure('Header.TLabel', font=('Segoe UI', 13, 'bold')) style.configure('Success.TLabel', foreground='#27ae60', font=('Segoe UI', 10, 'bold')) style.configure('Danger.TLabel', foreground='#e74c3c', font=('Segoe UI', 10, 'bold')) style.configure('Info.TLabel', foreground='#4a90d9', font=('Segoe UI', 10, 'bold')) style.configure('Action.TButton', font=('Segoe UI', 9)) style.configure('Start.TButton', font=('Segoe UI', 10, 'bold')) style.configure('Stop.TButton', font=('Segoe UI', 10, 'bold')) style.configure('TNotebook.Tab', font=('Segoe UI', 10), padding=[10, 5]) def load_config(self): if os.path.isfile(CONFIG_FILE): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f) except: pass return self.get_default_config() def get_default_config(self): return { "welcome_message": "👋 Добро пожаловать в Photo Print Bot!", "frame_enabled": False, "frame_color": "#000000", "frame_width": 10, "overlay_enabled": False, "overlay_text": "", "overlay_position": "bottom", "overlay_font_size": 24, "printer_name": "", "max_photos": 10, "max_copies": 100, "button_new_order": "📸 Новый заказ", "button_reset": "🔄 Сбросить", "button_help": "❓ Помощь", "button_add_photo": "📸 Добавить ещё фото", "button_ready": "✅ Готово, ввести количество", "button_cancel": "❌ Отменить заказ", "button_reset_all": "🔄 Сбросить всё", "button_preview": "👁 Предпросмотр", "button_confirm": "✅ Подтвердить печать", "text_start": "👋 Добро пожаловать в Photo Print Bot!\n\n🖨 Я помогу вам распечатать фото.\n\n📌 Чтобы начать, отправьте фото или нажмите кнопку '📸 Новый заказ'.", "text_new_order": "📸 НАЧИНАЕМ НОВЫЙ ЗАКАЗ\n\nОтправьте фото (можно несколько).\nПосле отправки всех фото нажмите '✅ Готово, ввести количество'.\n\n📊 Получено фото: 0/{max_photos}\n💡 Подсказка: можно отправить до {max_photos} фото", "text_photo_added": "✅ Фото добавлено! ({count}/{max_photos})\n\n📊 Получено фото: {count}\n📎 Осталось места: {remaining}\n\n📝 Что дальше:\n• Отправьте ещё фото\n• Или нажмите '✅ Готово, ввести количество'", "text_photo_limit": "⚠️ Достигнут лимит - {max_photos} фото.\nНажмите '✅ Готово, ввести количество' чтобы продолжить.", "text_no_photos": "❌ Вы не отправили ни одного фото.\n\nСначала отправьте фото, а затем нажмите '✅ Готово'.", "text_enter_count": "📸 У вас {count} фото.\n\n📝 Введите количество копий (число от 1 до {max_copies}).\n\n💡 Пример: 2 - значит каждое фото будет распечатано 2 раза.", "text_invalid_count": "❌ Введите число от 1 до {max_copies}.\n\n💡 Например: 2 - для двух копий каждого фото.", "text_order_summary": "📋 ПРОВЕРЬТЕ ЗАКАЗ\n\n📸 Фото: {photos}\n📎 Копий каждого: {copies}\n📊 Всего отпечатков: {total}\n\n✅ Всё верно? Нажмите '✅ Подтвердить печать'\n👁 Хотите посмотреть как будет выглядеть фото? Нажмите 'Предпросмотр'\n❌ Передумали? Нажмите '❌ Отменить'", "text_preview_loading": "⏳ Генерирую предпросмотр... Пожалуйста, подождите...", "text_preview_ready": "👁 ПРЕДПРОСМОТР\n\n📎 Всего фото: {photos}\n📋 Копий каждого: {copies}\n📊 Всего отпечатков: {total}\n🎨 Рамка: {'✅ Включена' if frame else '❌ Выключена'}\n✏️ Текст: {text or 'Нет'}\n\n✅ Всё устраивает? Нажмите 'Подтвердить печать'", "text_preview_error": "❌ Ошибка при создании предпросмотра.\n\nПопробуйте снова или нажмите 'Подтвердить печать'.", "text_loading_photos": "⏳ Загружаю фото и ставлю в очередь печати...\n\nПожалуйста, подождите...", "text_loading_progress": "⏳ Загружаю фото... {current}/{total}", "text_order_success": "✅ ЗАКАЗ ПРИНЯТ!\n\n📸 Все {photos} фото добавлены в очередь печати!\n📎 Копий каждого: {copies}\n📊 Всего отпечатков: {total}\n\n🖨 Печать начнётся автоматически.\n\n📌 Хотите сделать ещё один заказ? Нажмите '📸 Новый заказ'", "text_order_cancelled": "❌ Заказ отменён.\n\nНажмите '📸 Новый заказ' чтобы начать снова.", "text_reset": "🔄 Всё сброшено.\n\nНажмите '📸 Новый заказ' чтобы начать.", "text_help": "📖 КАК РАБОТАЕТ БОТ\n\n1️⃣ Отправьте фото или нажмите '📸 Новый заказ'\n2️⃣ Можно отправить до {max_photos} фото\n3️⃣ Нажмите '✅ Готово, ввести количество'\n4️⃣ Введите число от 1 до {max_copies} (количество копий)\n5️⃣ Подтвердите заказ\n\n🔹 КНОПКИ:\n• '📸 Новый заказ' - начать заказ\n• '🔄 Сбросить' - очистить всё\n• '❓ Помощь' - эта справка\n\n⚠️ ВАЖНО:\n• Количество копий для ВСЕХ фото сразу\n• Можно отправить до {max_photos} фото за раз\n• Печать начинается автоматически", "text_unknown": "❓ Я не понимаю эту команду.\n\n🔹 Используйте кнопки для управления:\n• '📸 Новый заказ' - начать заказ\n• '🔄 Сбросить' - очистить всё\n• '❓ Помощь' - справка\n\n📸 Или просто отправьте фото!" } def save_config(self): try: with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(self.config, f, indent=2, ensure_ascii=False) self.status_var.set("✅ Настройки сохранены") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}") def load_total_counter(self): counter_file = "total_photos_counter.txt" if os.path.isfile(counter_file): try: with open(counter_file, "r") as f: self.total_photos_printed = int(f.read().strip()) except: self.total_photos_printed = 0 def save_total_counter(self): try: with open("total_photos_counter.txt", "w") as f: f.write(str(self.total_photos_printed)) except: pass # ======================================================================== # ИНТЕРФЕЙС # ======================================================================== def create_widgets(self): # Основной контейнер с весом main = tk.Frame(self.root, bg='#f0f2f5') main.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.create_top_panel(main) self.create_notebook(main) self.create_status_bar(main) def create_top_panel(self, parent): top = tk.Frame(parent, bg='#f0f2f5') top.pack(fill=tk.X, pady=(0, 10)) # Заголовок title = tk.Frame(top, bg='#f0f2f5') title.pack(side=tk.LEFT, fill=tk.X, expand=True) tk.Label(title, text="🖨 Print Bot Admin", font=('Segoe UI', 18, 'bold'), bg='#f0f2f5', fg='#1a1a2e').pack(side=tk.LEFT) tk.Label(title, text="v3.0", font=('Segoe UI', 11), bg='#f0f2f5', fg='#6b7280').pack(side=tk.LEFT, padx=8) # Кнопки управления btn_frame = tk.Frame(top, bg='#f0f2f5') btn_frame.pack(side=tk.RIGHT) btn_primary = {'font': ('Segoe UI', 10, 'bold'), 'bg': '#4a90d9', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 15, 'pady': 6, 'cursor': 'hand2', 'bd': 0} btn_danger = {'font': ('Segoe UI', 10, 'bold'), 'bg': '#e74c3c', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 15, 'pady': 6, 'cursor': 'hand2', 'bd': 0} btn_sec = {'font': ('Segoe UI', 10), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 15, 'pady': 6, 'cursor': 'hand2', 'bd': 1} self.start_btn = tk.Button(btn_frame, text="▶ Запустить", command=self.start_bot, **btn_primary) self.start_btn.pack(side=tk.LEFT, padx=2) self.stop_btn = tk.Button(btn_frame, text="⏹ Остановить", command=self.stop_bot, state=tk.DISABLED, **btn_danger) self.stop_btn.pack(side=tk.LEFT, padx=2) self.restart_btn = tk.Button(btn_frame, text="🔄 Перезапустить", command=self.restart_bot, state=tk.DISABLED, **btn_sec) self.restart_btn.pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="🚫 Блокировка", command=self.open_blacklist, **btn_sec).pack(side=tk.LEFT, padx=5) # Статус status_frame = tk.Frame(top, bg='#f0f2f5') status_frame.pack(side=tk.RIGHT, padx=10) tk.Label(status_frame, text="Статус:", font=('Segoe UI', 10), bg='#f0f2f5', fg='#1a1a2e').pack(side=tk.LEFT) self.status_label = tk.Label(status_frame, text="⏹ Остановлен", font=('Segoe UI', 10, 'bold'), bg='#f0f2f5', fg='#e74c3c') self.status_label.pack(side=tk.LEFT, padx=5) # Разделитель sep = tk.Frame(parent, bg='#d1d5db', height=1) sep.pack(fill=tk.X, pady=8) # Статистика stats = tk.Frame(parent, bg='#f0f2f5') stats.pack(fill=tk.X) self.create_stat_card(stats, "📸 За сессию", "0", 0) self.create_stat_card(stats, "📊 Всего", "0", 1) self.create_stat_card(stats, "👥 Пользователей", "0", 2) tk.Button(stats, text="↺ Сбросить сессию", command=self.reset_session_counter, font=('Segoe UI', 10), bg='#f0f2f5', fg='#1a1a2e', relief=tk.FLAT, padx=15, pady=5, cursor='hand2', bd=1).pack(side=tk.RIGHT, padx=5) def create_stat_card(self, parent, label, value, col): frame = tk.Frame(parent, bg='#ffffff', relief=tk.FLAT, bd=1) frame.pack(side=tk.LEFT, padx=10) tk.Label(frame, text=label, font=('Segoe UI', 10), bg='#ffffff', fg='#6b7280').pack(padx=20, pady=(8, 0)) if col == 0: self.session_label = tk.Label(frame, text=value, font=('Segoe UI', 20, 'bold'), bg='#ffffff', fg='#4a90d9') self.session_label.pack(padx=20, pady=(0, 8)) elif col == 1: self.total_label = tk.Label(frame, text=value, font=('Segoe UI', 20, 'bold'), bg='#ffffff', fg='#27ae60') self.total_label.pack(padx=20, pady=(0, 8)) else: self.users_label = tk.Label(frame, text=value, font=('Segoe UI', 20, 'bold'), bg='#ffffff', fg='#4a90d9') self.users_label.pack(padx=20, pady=(0, 8)) def create_notebook(self, parent): self.notebook = ttk.Notebook(parent) self.notebook.pack(fill=tk.BOTH, expand=True, pady=5) self.create_log_tab() self.create_stats_tab() self.create_texts_tab() self.create_settings_tab() # ---------- Логи ---------- def create_log_tab(self): frame = tk.Frame(self.notebook, bg='#f0f2f5') self.notebook.add(frame, text="📋 Логи") # Фильтр filter_frame = tk.LabelFrame(frame, text="🔍 Фильтр", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) filter_frame.pack(fill=tk.X, pady=5, padx=5) row = tk.Frame(filter_frame, bg='#ffffff') row.pack(fill=tk.X, pady=2) types = [("error", "❌ Ошибки", "#e74c3c"), ("warning", "⚠️ Предупреждения", "#f39c12"), ("success", "✅ Успех", "#27ae60"), ("print", "🖨 Печать", "#4a90d9"), ("info", "ℹ️ Инфо", "#6b7280")] for key, label, color in types: var = tk.BooleanVar(value=False) self.filter_vars[key] = var cb = tk.Checkbutton(row, text=label, variable=var, bg='#ffffff', fg='#1a1a2e', command=self.apply_filter, font=('Segoe UI', 9)) cb.pack(side=tk.LEFT, padx=10) search_frame = tk.Frame(filter_frame, bg='#ffffff') search_frame.pack(fill=tk.X, pady=(5, 0)) tk.Label(search_frame, text="🔍 Поиск:", bg='#ffffff', font=('Segoe UI', 10)).pack(side=tk.LEFT, padx=5) self.search_entry = tk.Entry(search_frame, width=30, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) self.search_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) self.search_entry.bind('<KeyRelease>', self.apply_filter) btn_sec = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 10, 'pady': 3, 'cursor': 'hand2', 'bd': 1} tk.Button(search_frame, text="✕ Очистить", command=self.clear_filter, **btn_sec).pack(side=tk.LEFT, padx=5) # Управление control = tk.Frame(frame, bg='#f0f2f5') control.pack(fill=tk.X, pady=5, padx=5) btn_sec2 = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 10, 'pady': 4, 'cursor': 'hand2', 'bd': 1} tk.Button(control, text="🧹 Очистить", command=self.clear_logs, **btn_sec2).pack(side=tk.LEFT, padx=2) tk.Button(control, text="💾 Сохранить", command=self.export_logs, **btn_sec2).pack(side=tk.LEFT, padx=2) tk.Button(control, text="📋 Копировать", command=self.copy_logs, **btn_sec2).pack(side=tk.LEFT, padx=2) self.log_count = tk.Label(control, text="Строк: 0", font=('Segoe UI', 9), bg='#f0f2f5', fg='#6b7280') self.log_count.pack(side=tk.RIGHT, padx=5) # Текст логов self.log_text = scrolledtext.ScrolledText(frame, wrap=tk.WORD, state=tk.DISABLED, font=('Consolas', 10), background='#1e1e2e', foreground='#cdd6f4', insertbackground='#cba6f7', selectbackground='#45475a') self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.log_text.tag_config('error', foreground='#f38ba8') self.log_text.tag_config('warning', foreground='#fab387') self.log_text.tag_config('success', foreground='#a6e3a1') self.log_text.tag_config('info', foreground='#89b4fa') self.log_text.tag_config('print', foreground='#94e2d5') self.log_menu = tk.Menu(self.log_text, tearoff=0) self.log_menu.add_command(label="📋 Копировать", command=self.copy_logs) self.log_menu.add_command(label="🧹 Очистить", command=self.clear_logs) self.log_text.bind("<Button-3>", self.show_log_menu) # ---------- Статистика ---------- def create_stats_tab(self): frame = tk.Frame(self.notebook, bg='#f0f2f5') self.notebook.add(frame, text="📊 Статистика") summary = tk.LabelFrame(frame, text="Сводка", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) summary.pack(fill=tk.X, padx=10, pady=5) self.total_stats = tk.Label(summary, text="Всего распечатано фото: 0", font=('Segoe UI', 12, 'bold'), bg='#ffffff', fg='#1a1a2e') self.total_stats.pack(side=tk.LEFT, padx=20) self.session_stats = tk.Label(summary, text="За сессию: 0", font=('Segoe UI', 11), bg='#ffffff', fg='#4a90d9') self.session_stats.pack(side=tk.LEFT, padx=20) btn_danger = {'font': ('Segoe UI', 9), 'bg': '#e74c3c', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 12, 'pady': 4, 'cursor': 'hand2', 'bd': 0} tk.Button(summary, text="🗑 Очистить статистику", command=self.clear_stats, **btn_danger).pack(side=tk.RIGHT, padx=10) # Таблица table_frame = tk.Frame(frame, bg='#f0f2f5') table_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) scroll_y = tk.Scrollbar(table_frame) scroll_y.pack(side=tk.RIGHT, fill=tk.Y) self.stats_tree = ttk.Treeview(table_frame, columns=("user", "id", "total", "last"), show="headings", yscrollcommand=scroll_y.set, height=15) scroll_y.config(command=self.stats_tree.yview) self.stats_tree.heading("user", text="👤 Пользователь") self.stats_tree.heading("id", text="🆔 ID") self.stats_tree.heading("total", text="📊 Всего копий") self.stats_tree.heading("last", text="📅 Последний заказ") self.stats_tree.column("user", width=200) self.stats_tree.column("id", width=100) self.stats_tree.column("total", width=100) self.stats_tree.column("last", width=200) self.stats_tree.pack(fill=tk.BOTH, expand=True) btn_frame = tk.Frame(frame, bg='#f0f2f5') btn_frame.pack(pady=5) btn_sec = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 12, 'pady': 4, 'cursor': 'hand2', 'bd': 1} tk.Button(btn_frame, text="🔄 Обновить (F5)", command=self.refresh_stats, **btn_sec).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="📊 Экспорт CSV", command=self.export_stats, **btn_sec).pack(side=tk.LEFT, padx=5) # ---------- Тексты ---------- def create_texts_tab(self): frame = tk.Frame(self.notebook, bg='#f0f2f5') self.notebook.add(frame, text="✏️ Тексты") # Информация info = tk.Frame(frame, bg='#f0f2f5') info.pack(fill=tk.X, padx=10, pady=5) tk.Label(info, text="💡 Нажмите ✏️ для редактирования текста с предпросмотром", font=('Segoe UI', 10), bg='#f0f2f5', fg='#6b7280').pack(side=tk.LEFT) tk.Label(info, text="| Ctrl+S - сохранить все", font=('Segoe UI', 10), bg='#f0f2f5', fg='#6b7280').pack(side=tk.LEFT, padx=10) # Кнопки btn_frame = tk.Frame(frame, bg='#f0f2f5') btn_frame.pack(fill=tk.X, padx=10, pady=5) btn_primary = {'font': ('Segoe UI', 10, 'bold'), 'bg': '#4a90d9', 'fg': '#ffffff', 'relief': tk.FLAT, 'padx': 15, 'pady': 5, 'cursor': 'hand2', 'bd': 0} btn_sec = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 12, 'pady': 5, 'cursor': 'hand2', 'bd': 1} tk.Button(btn_frame, text="💾 Сохранить все (Ctrl+S)", command=self.save_all_texts, **btn_primary).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="🔄 Сбросить", command=self.reset_all_texts, **btn_sec).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="👁 Предпросмотр", command=self.show_all_texts_preview, **btn_sec).pack(side=tk.LEFT, padx=2) # Скроллируемая область canvas = tk.Canvas(frame, highlightthickness=0, bg='#f0f2f5') scrollbar = tk.Scrollbar(frame, orient="vertical", command=canvas.yview) scrollable = tk.Frame(canvas, bg='#f0f2f5') scrollable.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scrollable, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True, padx=5, pady=5) scrollbar.pack(side="right", fill="y", pady=5) # Группа кнопок group_btns = tk.LabelFrame(scrollable, text="🔘 Кнопки", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group_btns.pack(fill=tk.X, padx=10, pady=5) self.button_texts = {} fields = [ ("button_new_order", "📸 Новый заказ:"), ("button_reset", "🔄 Сбросить:"), ("button_help", "❓ Помощь:"), ("button_add_photo", "📸 Добавить фото:"), ("button_ready", "✅ Готово:"), ("button_cancel", "❌ Отменить:"), ("button_reset_all", "🔄 Сбросить всё:"), ("button_preview", "👁 Предпросмотр:"), ("button_confirm", "✅ Подтвердить:"), ] for key, label in fields: f = tk.Frame(group_btns, bg='#ffffff') f.pack(fill=tk.X, pady=3) tk.Label(f, text=label, width=18, font=('Segoe UI', 9), bg='#ffffff', fg='#1a1a2e', anchor=tk.W).pack(side=tk.LEFT) var = tk.StringVar(value=self.config.get(key, "")) entry = tk.Entry(f, textvariable=var, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) btn_edit = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 8, 'pady': 2, 'cursor': 'hand2', 'bd': 1} tk.Button(f, text="✏️", command=lambda k=key, v=var: self.open_text_editor(k, v, False), **btn_edit).pack(side=tk.LEFT, padx=2) self.button_texts[key] = var # Группа сообщений group_msg = tk.LabelFrame(scrollable, text="💬 Сообщения", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group_msg.pack(fill=tk.X, padx=10, pady=5) self.message_texts = {} msg_fields = [ ("text_start", "👋 Приветствие:"), ("text_new_order", "📸 Новый заказ:"), ("text_photo_added", "🖼 Фото добавлено:"), ("text_photo_limit", "⚠️ Лимит фото:"), ("text_no_photos", "❌ Нет фото:"), ("text_enter_count", "🔢 Введите количество:"), ("text_invalid_count", "❌ Неверное число:"), ("text_order_summary", "📋 Сводка заказа:"), ("text_preview_loading", "⏳ Загрузка предпросмотра:"), ("text_preview_ready", "👁 Предпросмотр:"), ("text_preview_error", "❌ Ошибка предпросмотра:"), ("text_loading_photos", "⏳ Загрузка фото:"), ("text_loading_progress", "📊 Прогресс:"), ("text_order_success", "✅ Успешный заказ:"), ("text_order_cancelled", "❌ Отменен:"), ("text_reset", "🔄 Сброс:"), ("text_unknown", "❓ Неизвестно:"), ] for key, label in msg_fields: f = tk.Frame(group_msg, bg='#ffffff') f.pack(fill=tk.X, pady=3) tk.Label(f, text=label, width=22, font=('Segoe UI', 9), bg='#ffffff', fg='#1a1a2e', anchor=tk.W).pack(side=tk.LEFT) var = tk.StringVar(value=self.config.get(key, "")) entry = tk.Entry(f, textvariable=var, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) btn_edit = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 8, 'pady': 2, 'cursor': 'hand2', 'bd': 1} tk.Button(f, text="✏️", command=lambda k=key, v=var: self.open_text_editor(k, v, True), **btn_edit).pack(side=tk.LEFT, padx=2) self.message_texts[key] = var # ---------- Настройки ---------- def create_settings_tab(self): frame = tk.Frame(self.notebook, bg='#f0f2f5') self.notebook.add(frame, text="⚙️ Настройки") # Скроллируемая область canvas = tk.Canvas(frame, highlightthickness=0, bg='#f0f2f5') scrollbar = tk.Scrollbar(frame, orient="vertical", command=canvas.yview) scrollable = tk.Frame(canvas, bg='#f0f2f5') scrollable.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scrollable, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True, padx=5, pady=5) scrollbar.pack(side="right", fill="y", pady=5) # Рамка group = tk.LabelFrame(scrollable, text="🎨 Рамка", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group.pack(fill=tk.X, padx=10, pady=5) self.frame_var = tk.BooleanVar(value=self.config.get("frame_enabled", False)) tk.Checkbutton(group, text="✅ Включить рамку", variable=self.frame_var, bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10), command=self.save_frame).pack(anchor=tk.W, pady=2) color_frame = tk.Frame(group, bg='#ffffff') color_frame.pack(fill=tk.X, pady=5) tk.Label(color_frame, text="Цвет:", bg='#ffffff', font=('Segoe UI', 10)).pack(side=tk.LEFT, padx=5) self.color_preview = tk.Label(color_frame, bg=self.config.get("frame_color", "#000000"), width=10, height=2, relief=tk.RAISED) self.color_preview.pack(side=tk.LEFT, padx=5) btn_sec = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 10, 'pady': 3, 'cursor': 'hand2', 'bd': 1} tk.Button(color_frame, text="Выбрать", command=self.choose_color, **btn_sec).pack(side=tk.LEFT, padx=5) width_frame = tk.Frame(group, bg='#ffffff') width_frame.pack(fill=tk.X, pady=5) tk.Label(width_frame, text="Толщина (px):", bg='#ffffff', font=('Segoe UI', 10)).pack(side=tk.LEFT, padx=5) self.width_var = tk.IntVar(value=self.config.get("frame_width", 10)) spin = tk.Spinbox(width_frame, from_=1, to=100, textvariable=self.width_var, width=8, relief=tk.FLAT, borderwidth=1, highlightthickness=1, command=self.save_frame) spin.pack(side=tk.LEFT, padx=5) # Оверлей group2 = tk.LabelFrame(scrollable, text="✏️ Оверлей", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group2.pack(fill=tk.X, padx=10, pady=5) self.overlay_enabled_var = tk.BooleanVar(value=self.config.get("overlay_enabled", False)) tk.Checkbutton(group2, text="✅ Включить оверлей", variable=self.overlay_enabled_var, bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10), command=self.save_overlay).pack(anchor=tk.W, pady=2) tk.Label(group2, text="Текст:", bg='#ffffff', font=('Segoe UI', 10)).pack(anchor=tk.W, pady=2) entry_frame = tk.Frame(group2, bg='#ffffff') entry_frame.pack(fill=tk.X, pady=2) self.overlay_var = tk.StringVar(value=self.config.get("overlay_text", "")) entry = tk.Entry(entry_frame, textvariable=self.overlay_var, font=('Segoe UI', 10), relief=tk.FLAT, borderwidth=1, highlightthickness=1) entry.pack(side=tk.LEFT, fill=tk.X, expand=True) tk.Button(entry_frame, text="✏️", command=lambda: self.open_text_editor("overlay_text", self.overlay_var, False), **btn_sec).pack(side=tk.LEFT, padx=2) pos_frame = tk.Frame(group2, bg='#ffffff') pos_frame.pack(fill=tk.X, pady=5) tk.Label(pos_frame, text="Позиция:", bg='#ffffff', font=('Segoe UI', 10)).pack(side=tk.LEFT, padx=5) self.pos_var = tk.StringVar(value=self.config.get("overlay_position", "bottom")) tk.Radiobutton(pos_frame, text="⬇ Снизу", variable=self.pos_var, value="bottom", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10), command=self.save_overlay).pack(side=tk.LEFT, padx=5) tk.Radiobutton(pos_frame, text="⬆ Сверху", variable=self.pos_var, value="top", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10), command=self.save_overlay).pack(side=tk.LEFT, padx=5) font_frame = tk.Frame(group2, bg='#ffffff') font_frame.pack(fill=tk.X, pady=5) tk.Label(font_frame, text="Размер шрифта:", bg='#ffffff', font=('Segoe UI', 10)).pack(side=tk.LEFT, padx=5) self.font_var = tk.IntVar(value=self.config.get("overlay_font_size", 24)) spin2 = tk.Spinbox(font_frame, from_=8, to=72, textvariable=self.font_var, width=8, relief=tk.FLAT, borderwidth=1, highlightthickness=1, command=self.save_overlay) spin2.pack(side=tk.LEFT, padx=5) tk.Button(group2, text="💾 Применить", command=self.save_overlay, **btn_sec).pack(pady=5) # Принтер group3 = tk.LabelFrame(scrollable, text="🖨 Принтер", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group3.pack(fill=tk.X, padx=10, pady=5) current_printer = self.config.get("printer_name", "") or "по умолчанию" self.printer_label = tk.Label(group3, text=f"Текущий: {current_printer}", font=('Segoe UI', 10, 'bold'), bg='#ffffff', fg='#4a90d9') self.printer_label.pack(anchor=tk.W, pady=5) tk.Button(group3, text="🔄 Выбрать принтер", command=self.select_printer, **btn_sec).pack(pady=5) # Системные group4 = tk.LabelFrame(scrollable, text="⚙️ Системные", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group4.pack(fill=tk.X, padx=10, pady=5) sys_grid = tk.Frame(group4, bg='#ffffff') sys_grid.pack(fill=tk.X) tk.Label(sys_grid, text="Максимум фото:", bg='#ffffff', font=('Segoe UI', 10)).grid(row=0, column=0, padx=5, pady=2, sticky=tk.W) self.max_photos_var = tk.IntVar(value=self.config.get("max_photos", 10)) spin3 = tk.Spinbox(sys_grid, from_=1, to=20, textvariable=self.max_photos_var, width=8, relief=tk.FLAT, borderwidth=1, highlightthickness=1) spin3.grid(row=0, column=1, padx=5, pady=2, sticky=tk.W) tk.Label(sys_grid, text="Максимум копий:", bg='#ffffff', font=('Segoe UI', 10)).grid(row=1, column=0, padx=5, pady=2, sticky=tk.W) self.max_copies_var = tk.IntVar(value=self.config.get("max_copies", 100)) spin4 = tk.Spinbox(sys_grid, from_=1, to=500, textvariable=self.max_copies_var, width=8, relief=tk.FLAT, borderwidth=1, highlightthickness=1) spin4.grid(row=1, column=1, padx=5, pady=2, sticky=tk.W) tk.Button(group4, text="💾 Сохранить", command=self.save_system_settings, **btn_sec).pack(pady=5) # Утилиты group5 = tk.LabelFrame(scrollable, text="🛠 Утилиты", bg='#ffffff', fg='#1a1a2e', font=('Segoe UI', 10, 'bold'), padx=10, pady=10) group5.pack(fill=tk.X, padx=10, pady=5) utils = tk.Frame(group5, bg='#ffffff') utils.pack(fill=tk.X) tk.Button(utils, text="🧹 Очистить temp", command=self.clear_temp, **btn_sec).pack(side=tk.LEFT, padx=5) tk.Button(utils, text="📁 Открыть photos", command=self.open_photos, **btn_sec).pack(side=tk.LEFT, padx=5) tk.Button(utils, text="🔄 Автообновление", command=self.toggle_auto_refresh, **btn_sec).pack(side=tk.LEFT, padx=5) # ---------- Статус бар ---------- def create_status_bar(self, parent): status_frame = tk.Frame(parent, bg='#f0f2f5') status_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0)) self.status_var = tk.StringVar(value="✅ Готов") self.status_bar = tk.Label(status_frame, textvariable=self.status_var, anchor=tk.W, padx=10, pady=4, font=('Segoe UI', 9), bg='#ffffff', relief=tk.FLAT, bd=1) self.status_bar.pack(side=tk.LEFT, fill=tk.X, expand=True) tk.Label(status_frame, text="|", font=('Segoe UI', 9), bg='#f0f2f5', fg='#d1d5db').pack(side=tk.LEFT, padx=5) self.time_label = tk.Label(status_frame, text=datetime.now().strftime("%H:%M:%S"), font=('Segoe UI', 9), bg='#f0f2f5', fg='#6b7280') self.time_label.pack(side=tk.RIGHT, padx=5) self.update_time() def update_time(self): self.time_label.config(text=datetime.now().strftime("%H:%M:%S")) self.root.after(1000, self.update_time) # ======================================================================== # МЕТОДЫ РЕДАКТОРА # ======================================================================== def open_text_editor(self, key, var, is_multiline): text = var.get() title = key.replace("_", " ").title() ModernTextEditor(self.root, title, text, lambda v: self.save_text_callback(key, var, v), is_multiline) def save_text_callback(self, key, var, text): var.set(text) self.config[key] = text self.save_config() self.status_var.set(f"✅ Обновлено: {key}") # ======================================================================== # МЕТОДЫ ЛОГОВ # ======================================================================== def log(self, text, tag=None): self.all_log_lines.append(text) try: self.log_text.config(state=tk.NORMAL) if tag: self.log_text.insert(tk.END, text, tag) else: self.log_text.insert(tk.END, text) self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) self.update_log_count() except: pass def update_log_count(self): try: count = len(self.log_text.get("1.0", tk.END).splitlines()) self.log_count.config(text=f"Строк: {count}") except: pass def apply_filter(self, event=None): search_text = self.search_entry.get().strip().lower() active = [k for k, v in self.filter_vars.items() if v.get()] self.log_text.config(state=tk.NORMAL) self.log_text.delete("1.0", tk.END) filtered = 0 for line in self.all_log_lines: show = True if active: show = any(f in line.lower() for f in active) if show and search_text: show = search_text in line.lower() if show: self.log_text.insert(tk.END, line) filtered += 1 self.log_text.config(state=tk.DISABLED) self.log_count.config(text=f"Строк: {filtered} / {len(self.all_log_lines)}") def clear_filter(self): self.search_entry.delete(0, tk.END) for var in self.filter_vars.values(): var.set(False) self.apply_filter() def focus_search(self): self.search_entry.focus_set() self.search_entry.select_range(0, tk.END) def copy_logs(self): try: text = self.log_text.get("1.0", tk.END) if text.strip(): self.root.clipboard_clear() self.root.clipboard_append(text) self.status_var.set(f"📋 Скопировано {len(text)} символов") except: pass def show_log_menu(self, event): try: self.log_menu.post(event.x_root, event.y_root) finally: self.log_menu.grab_release() def clear_logs(self): if messagebox.askyesno("Подтверждение", "Очистить логи?"): self.log_text.config(state=tk.NORMAL) self.log_text.delete("1.0", tk.END) self.log_text.config(state=tk.DISABLED) self.all_log_lines = [] self.log_count.config(text="Строк: 0") def export_logs(self): try: content = self.log_text.get("1.0", tk.END) if not content.strip(): messagebox.showwarning("Предупреждение", "Логи пусты") return file_path = filedialog.asksaveasfilename(defaultextension=".log", filetypes=[("Log files", "*.log"), ("Text files", "*.txt")], initialfile=f"logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") if file_path: with open(file_path, "w", encoding="utf-8") as f: f.write(content) self.status_var.set("📄 Логи сохранены") messagebox.showinfo("Успех", f"Логи сохранены") except Exception as e: messagebox.showerror("Ошибка", str(e)) # ======================================================================== # МЕТОДЫ СТАТИСТИКИ # ======================================================================== def update_all_stats(self): self.update_counter_display() self.refresh_stats() def update_counter_display(self): self.session_label.config(text=str(self.session_photos_printed)) self.total_label.config(text=str(self.total_photos_printed)) self.session_stats.config(text=f"За сессию: {self.session_photos_printed}") self.total_stats.config(text=f"Всего распечатано фото: {self.total_photos_printed}") users_count = len(self.stats_tree.get_children()) if hasattr(self, 'stats_tree') else 0 self.users_label.config(text=str(users_count)) def refresh_stats(self): for item in self.stats_tree.get_children(): self.stats_tree.delete(item) if not os.path.isfile(STATS_FILE): return try: users = {} with open(STATS_FILE, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: uid = row["user_id"] if uid not in users: users[uid] = {"username": row["username"], "total": 0, "last": row["timestamp"]} users[uid]["total"] += int(row["copies"]) if row["timestamp"] > users[uid]["last"]: users[uid]["last"] = row["timestamp"] for uid, data in users.items(): self.stats_tree.insert("", tk.END, values=( data["username"], uid, data["total"], data["last"][:16] )) self.users_label.config(text=str(len(users))) except Exception as e: self.log(f"Ошибка статистики: {e}\n", 'error') def clear_stats(self): if messagebox.askyesno("Предупреждение", "Очистить всю статистику? Это необратимо!"): try: with open(STATS_FILE, "w", newline="", encoding="utf-8") as f: csv.writer(f).writerow(["timestamp", "user_id", "username", "copies"]) self.total_photos_printed = 0 self.session_photos_printed = 0 self.save_total_counter() self.update_counter_display() self.refresh_stats() self.log("🗑 Статистика очищена\n", 'success') messagebox.showinfo("Успех", "Статистика очищена") except Exception as e: messagebox.showerror("Ошибка", str(e)) def export_stats(self): if not os.path.isfile(STATS_FILE): messagebox.showwarning("Предупреждение", "Файл статистики не найден") return try: file_path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files", "*.csv")], initialfile=f"stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv") if file_path: import shutil shutil.copy2(STATS_FILE, file_path) self.status_var.set("📊 Статистика экспортирована") messagebox.showinfo("Успех", f"Статистика сохранена") except Exception as e: messagebox.showerror("Ошибка", str(e)) def increment_counter(self, count=1): self.total_photos_printed += count self.session_photos_printed += count self.save_total_counter() self.update_counter_display() def reset_session_counter(self): if messagebox.askyesno("Подтверждение", "Сбросить счетчик сессии?"): self.session_photos_printed = 0 self.update_counter_display() self.log("🔄 Счетчик сессии сброшен\n", 'info') # ======================================================================== # МЕТОДЫ СОХРАНЕНИЯ ТЕКСТОВ # ======================================================================== def save_all_texts(self): for key, var in self.button_texts.items(): self.config[key] = var.get().strip() for key, var in self.message_texts.items(): self.config[key] = var.get().strip() self.save_config() self.status_var.set("💾 Все тексты сохранены") messagebox.showinfo("Успех", "Все тексты сохранены!") def reset_all_texts(self): if messagebox.askyesno("Подтверждение", "Сбросить все тексты к стандартным?"): default = self.get_default_config() for key, var in self.button_texts.items(): var.set(default.get(key, "")) self.config[key] = default.get(key, "") for key, var in self.message_texts.items(): var.set(default.get(key, "")) self.config[key] = default.get(key, "") self.save_config() self.status_var.set("🔄 Тексты сброшены") messagebox.showinfo("Успех", "Тексты сброшены!") def show_all_texts_preview(self): preview = "=== ПРЕДПРОСМОТР ВСЕХ ТЕКСТОВ ===\n\n" preview += "🔘 КНОПКИ:\n" + "─" * 40 + "\n" for key, var in self.button_texts.items(): preview += f" {key}: {var.get()}\n" preview += "\n💬 СООБЩЕНИЯ:\n" + "─" * 40 + "\n" for key, var in self.message_texts.items(): text = var.get() preview += f" {key}: {text[:80]}{'...' if len(text) > 80 else ''}\n" dialog = tk.Toplevel(self.root) dialog.title("👁 Предпросмотр всех текстов") dialog.geometry("800x600") dialog.transient(self.root) dialog.configure(bg='#f0f2f5') text_widget = scrolledtext.ScrolledText(dialog, font=('Consolas', 10), wrap=tk.NONE, bg='#ffffff', fg='#1a1a2e') text_widget.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) text_widget.insert("1.0", preview) text_widget.config(state=tk.DISABLED) btn_frame = tk.Frame(dialog, bg='#f0f2f5') btn_frame.pack(pady=10) btn_sec = {'font': ('Segoe UI', 9), 'bg': '#f0f2f5', 'fg': '#1a1a2e', 'relief': tk.FLAT, 'padx': 15, 'pady': 5, 'cursor': 'hand2', 'bd': 1} def copy_preview(): self.root.clipboard_clear() self.root.clipboard_append(preview) self.status_var.set("📋 Скопировано") tk.Button(btn_frame, text="📋 Копировать", command=copy_preview, **btn_sec).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="❌ Закрыть", command=dialog.destroy, **btn_sec).pack(side=tk.LEFT, padx=5) dialog.transient(self.root) dialog.grab_set() dialog.focus_set() # ======================================================================== # МЕТОДЫ НАСТРОЕК # ======================================================================== def save_system_settings(self): self.config["max_photos"] = self.max_photos_var.get() self.config["max_copies"] = self.max_copies_var.get() self.save_config() self.status_var.set("💾 Системные настройки сохранены") messagebox.showinfo("Успех", "Системные настройки сохранены!") def save_frame(self): self.config["frame_enabled"] = self.frame_var.get() self.config["frame_width"] = self.width_var.get() self.save_config() self.status_var.set("💾 Настройки рамки сохранены") def choose_color(self): color = colorchooser.askcolor(title="Выберите цвет", color=self.config.get("frame_color", "#000000")) if color: self.config["frame_color"] = color[1] self.color_preview.config(bg=color[1]) self.save_config() self.status_var.set(f"🎨 Цвет выбран: {color[1]}") def save_overlay(self): self.config["overlay_enabled"] = self.overlay_enabled_var.get() self.config["overlay_text"] = self.overlay_var.get() self.config["overlay_position"] = self.pos_var.get() self.config["overlay_font_size"] = self.font_var.get() self.save_config() self.status_var.set("💾 Настройки оверлея сохранены") messagebox.showinfo("Успех", "Настройки оверлея сохранены!") def select_printer(self): current = self.config.get("printer_name", "") PrinterSelector(self.root, current, self.set_printer) def set_printer(self, printer): self.config["printer_name"] = printer self.save_config() self.printer_label.config(text=f"Текущий: {printer or 'по умолчанию'}") self.status_var.set(f"🖨 Выбран принтер: {printer or 'по умолчанию'}") self.log(f"🖨 Выбран принтер: {printer or 'по умолчанию'}\n", 'success') messagebox.showinfo("Успех", f"Принтер выбран: {printer or 'по умолчанию'}") # ======================================================================== # МЕТОДЫ БОТА # ======================================================================== def check_existing_bot(self): if not PSUTIL_AVAILABLE: return try: for proc in psutil.process_iter(['pid', 'name', 'cmdline']): try: cmd = ' '.join(proc.info['cmdline'] or []) if 'bot.py' in cmd and 'bot_control.py' not in cmd: self.log(f"⚠️ Найден процесс бота (PID: {proc.info['pid']})\n", 'warning') if messagebox.askyesno("Внимание", f"Найден запущенный бот (PID: {proc.info['pid']}). Остановить?"): proc.terminate() proc.wait(timeout=3) self.log("✅ Процесс бота остановлен\n", 'success') except: pass except: pass def start_bot(self): if self.running: return try: self.bot_process = subprocess.Popen( [sys.executable, BOT_SCRIPT], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1, cwd=os.path.dirname(os.path.abspath(BOT_SCRIPT)) ) self.running = True self.update_buttons() self.status_label.config(text="▶ Запущен", fg='#27ae60') self.log("✅ Бот запущен\n", 'success') self.stop_log_thread = False self.log_thread = threading.Thread(target=self.read_output, daemon=True) self.log_thread.start() except Exception as e: messagebox.showerror("Ошибка", f"Не удалось запустить: {e}") def stop_bot(self): if not self.running or not self.bot_process: return try: self.bot_process.terminate() self.bot_process.wait(timeout=3) self.running = False self.stop_log_thread = True self.update_buttons() self.status_label.config(text="⏹ Остановлен", fg='#e74c3c') self.log("⏹ Бот остановлен\n") except Exception as e: messagebox.showerror("Ошибка", f"Ошибка остановки: {e}") def restart_bot(self): if not self.running: return self.log("🔄 Перезапуск бота...\n", 'warning') self.stop_bot() self.root.after(1000, self.start_bot) def read_output(self): try: for line in iter(self.bot_process.stdout.readline, ''): if self.stop_log_thread: break self.log(line) if "Печать завершена" in line: self.increment_counter() except: pass finally: if self.bot_process and self.bot_process.stdout: self.bot_process.stdout.close() def update_buttons(self): if self.running: self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.restart_btn.config(state=tk.NORMAL) else: self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.restart_btn.config(state=tk.DISABLED) # ======================================================================== # УТИЛИТЫ # ======================================================================== def clear_temp(self): if messagebox.askyesno("Подтверждение", "Очистить папку temp?"): temp_dir = "temp" if os.path.exists(temp_dir): deleted = 0 for file in os.listdir(temp_dir): try: os.remove(os.path.join(temp_dir, file)) deleted += 1 except: pass self.log(f"🧹 Удалено {deleted} файлов из temp\n", 'info') self.status_var.set(f"🧹 Удалено {deleted} файлов") messagebox.showinfo("Успех", f"Удалено {deleted} файлов") def open_photos(self): folder = os.path.abspath(PHOTOS_DIR) os.makedirs(folder, exist_ok=True) try: os.startfile(folder) self.status_var.set(f"📁 Открыта папка: {folder}") except Exception as e: messagebox.showerror("Ошибка", str(e)) def open_blacklist(self): BlacklistManager(self.root, self.on_blacklist_changed).show_dialog() def on_blacklist_changed(self): self.status_var.set("🚫 Черный список обновлен") if self.running: self.log("🚫 Черный список обновлен\n", 'warning') def toggle_auto_refresh(self): self.auto_refresh = not self.auto_refresh self.status_var.set("🔄 Автообновление " + ("включено" if self.auto_refresh else "выключено")) def start_auto_refresh(self): if self.auto_refresh: self.refresh_stats() self.root.after(15000, self.start_auto_refresh) def refresh_all(self): self.refresh_stats() self.update_counter_display() self.status_var.set("🔄 Все данные обновлены") def on_closing(self): if self.running: if messagebox.askyesno("Подтверждение", "Бот запущен. Остановить и закрыть?"): self.stop_bot() self.root.destroy() else: self.root.destroy() # ======================================================================== # ЗАПУСК # ======================================================================== if __name__ == "__main__": root = tk.Tk() app = BotControlApp(root) root.mainloop()