|
|
@@ -0,0 +1,886 @@
|
|
|
+import tkinter as tk
|
|
|
+from tkinter import ttk, messagebox, scrolledtext, filedialog
|
|
|
+import json
|
|
|
+import base64
|
|
|
+from cryptography.fernet import Fernet
|
|
|
+import os
|
|
|
+import pyperclip
|
|
|
+from datetime import datetime
|
|
|
+import getpass
|
|
|
+import platform
|
|
|
+import random
|
|
|
+import string
|
|
|
+from tkinter import font as tkfont
|
|
|
+import time
|
|
|
+
|
|
|
+# Установите библиотеки если их нет:
|
|
|
+# pip install cryptography pyperclip
|
|
|
+
|
|
|
+class ModernPasswordManager:
|
|
|
+ def __init__(self, root):
|
|
|
+ self.root = root
|
|
|
+ self.root.title("🔐 SecurePass Manager")
|
|
|
+ self.root.geometry("1200x750")
|
|
|
+
|
|
|
+ # Устанавливаем иконку (если есть)
|
|
|
+ try:
|
|
|
+ self.root.iconbitmap('icon.ico') # Можно создать иконку
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # Центрирование окна
|
|
|
+ self.center_window()
|
|
|
+
|
|
|
+ # Современная цветовая схема
|
|
|
+ self.colors = {
|
|
|
+ 'primary': '#2A2D43', # Темно-синий
|
|
|
+ 'secondary': '#1E1F29', # Более темный фон
|
|
|
+ 'accent': '#4A6FA5', # Голубой акцент
|
|
|
+ 'success': '#50C878', # Изумрудный
|
|
|
+ 'warning': '#FF6B6B', # Красный/коралловый
|
|
|
+ 'text': '#E8EAED', # Светлый текст
|
|
|
+ 'entry_bg': '#3C3F58', # Фон полей ввода
|
|
|
+ 'card_bg': '#34374C', # Фон карточек
|
|
|
+ 'hover': '#5A6FA5', # Цвет при наведении
|
|
|
+ 'gradient_start': '#667eea', # Для градиентов
|
|
|
+ 'gradient_end': '#764ba2'
|
|
|
+ }
|
|
|
+
|
|
|
+ # Загружаем шрифты
|
|
|
+ self.load_fonts()
|
|
|
+
|
|
|
+ # Настройка стилей
|
|
|
+ self.setup_styles()
|
|
|
+
|
|
|
+ # Получаем путь к Документам
|
|
|
+ self.documents_path = self.get_documents_path()
|
|
|
+
|
|
|
+ # Инициализация файлов
|
|
|
+ self.data_dir = os.path.join(self.documents_path, "SecurePassManager")
|
|
|
+ os.makedirs(self.data_dir, exist_ok=True)
|
|
|
+
|
|
|
+ self.filename = os.path.join(self.data_dir, "passwords.enc")
|
|
|
+ self.keyfile = os.path.join(self.data_dir, "master.key")
|
|
|
+ self.backup_dir = os.path.join(self.data_dir, "backups")
|
|
|
+ os.makedirs(self.backup_dir, exist_ok=True)
|
|
|
+
|
|
|
+ # Загрузка или создание ключа
|
|
|
+ self.key = self.load_or_create_key()
|
|
|
+ if self.key:
|
|
|
+ self.cipher = Fernet(self.key)
|
|
|
+ self.passwords = self.load_passwords()
|
|
|
+ else:
|
|
|
+ self.show_error_screen("Не удалось инициализировать систему шифрования")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Создание интерфейса
|
|
|
+ self.create_main_layout()
|
|
|
+ self.update_service_list()
|
|
|
+
|
|
|
+ # Запуск анимаций
|
|
|
+ self.root.after(100, self.animate_welcome)
|
|
|
+
|
|
|
+ def center_window(self):
|
|
|
+ """Центрирует окно на экране"""
|
|
|
+ self.root.update_idletasks()
|
|
|
+ width = self.root.winfo_width()
|
|
|
+ height = self.root.winfo_height()
|
|
|
+ x = (self.root.winfo_screenwidth() // 2) - (width // 2)
|
|
|
+ y = (self.root.winfo_screenheight() // 2) - (height // 2)
|
|
|
+ self.root.geometry(f'{width}x{height}+{x}+{y}')
|
|
|
+
|
|
|
+ def load_fonts(self):
|
|
|
+ """Загружаем шрифты"""
|
|
|
+ self.title_font = ("Segoe UI", 24, "bold")
|
|
|
+ self.heading_font = ("Segoe UI", 14, "bold")
|
|
|
+ self.body_font = ("Segoe UI", 10)
|
|
|
+ self.mono_font = ("Consolas", 10)
|
|
|
+
|
|
|
+ def setup_styles(self):
|
|
|
+ """Настройка стилей для виджетов"""
|
|
|
+ style = ttk.Style()
|
|
|
+ style.theme_use('clam')
|
|
|
+
|
|
|
+ # Настраиваем стили
|
|
|
+ style.configure('Modern.TButton',
|
|
|
+ font=self.body_font,
|
|
|
+ borderwidth=0,
|
|
|
+ relief='flat',
|
|
|
+ padding=10)
|
|
|
+
|
|
|
+ style.configure('Card.TFrame',
|
|
|
+ background=self.colors['card_bg'],
|
|
|
+ relief='flat',
|
|
|
+ borderwidth=0)
|
|
|
+
|
|
|
+ style.configure('Primary.TButton',
|
|
|
+ font=self.body_font,
|
|
|
+ background=self.colors['accent'],
|
|
|
+ foreground=self.colors['text'],
|
|
|
+ borderwidth=0)
|
|
|
+
|
|
|
+ def get_documents_path(self):
|
|
|
+ """Получает путь к папке Документы"""
|
|
|
+ system = platform.system()
|
|
|
+
|
|
|
+ if system == "Windows":
|
|
|
+ import ctypes.wintypes
|
|
|
+ CSIDL_PERSONAL = 5
|
|
|
+ SHGFP_TYPE_CURRENT = 0
|
|
|
+ buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
|
|
|
+ ctypes.windll.shell32.SHGetFolderPathW(0, CSIDL_PERSONAL, 0, SHGFP_TYPE_CURRENT, buf)
|
|
|
+ return buf.value
|
|
|
+ elif system == "Darwin":
|
|
|
+ return os.path.expanduser("~/Documents")
|
|
|
+ else:
|
|
|
+ return os.path.expanduser("~/Documents")
|
|
|
+
|
|
|
+ def load_or_create_key(self):
|
|
|
+ """Загружает или создает ключ шифрования"""
|
|
|
+ if os.path.exists(self.keyfile):
|
|
|
+ try:
|
|
|
+ with open(self.keyfile, 'rb') as f:
|
|
|
+ return f.read()
|
|
|
+ except:
|
|
|
+ return None
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ key = Fernet.generate_key()
|
|
|
+ with open(self.keyfile, 'wb') as f:
|
|
|
+ f.write(key)
|
|
|
+ self.show_welcome_message()
|
|
|
+ return key
|
|
|
+ except:
|
|
|
+ return None
|
|
|
+
|
|
|
+ def show_welcome_message(self):
|
|
|
+ """Показывает приветственное сообщение"""
|
|
|
+ welcome_window = tk.Toplevel(self.root)
|
|
|
+ welcome_window.title("Добро пожаловать!")
|
|
|
+ welcome_window.geometry("500x400")
|
|
|
+ welcome_window.configure(bg=self.colors['primary'])
|
|
|
+ welcome_window.resizable(False, False)
|
|
|
+
|
|
|
+ # Центрируем
|
|
|
+ welcome_window.update_idletasks()
|
|
|
+ width = welcome_window.winfo_width()
|
|
|
+ height = welcome_window.winfo_height()
|
|
|
+ x = (welcome_window.winfo_screenwidth() // 2) - (width // 2)
|
|
|
+ y = (welcome_window.winfo_screenheight() // 2) - (height // 2)
|
|
|
+ welcome_window.geometry(f'{width}x{height}+{x}+{y}')
|
|
|
+
|
|
|
+ # Содержимое
|
|
|
+ frame = tk.Frame(welcome_window, bg=self.colors['primary'])
|
|
|
+ frame.pack(expand=True, fill='both', padx=40, pady=40)
|
|
|
+
|
|
|
+ tk.Label(frame, text="🔐", font=("Segoe UI", 48),
|
|
|
+ bg=self.colors['primary'], fg=self.colors['accent']).pack(pady=(0, 20))
|
|
|
+
|
|
|
+ tk.Label(frame, text="SecurePass Manager", font=self.title_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text']).pack(pady=(0, 10))
|
|
|
+
|
|
|
+ message = ("Добро пожаловать в SecurePass Manager!\n\n"
|
|
|
+ "• Все ваши пароли защищены современным шифрованием\n"
|
|
|
+ "• Ключ шифрования сохранен в безопасном месте\n"
|
|
|
+ "• Данные хранятся локально на вашем компьютере\n\n"
|
|
|
+ "🚨 ВАЖНО: Сохраните файл master.key в надежном месте!")
|
|
|
+
|
|
|
+ tk.Label(frame, text=message, font=self.body_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text'],
|
|
|
+ justify=tk.LEFT).pack(pady=(0, 30))
|
|
|
+
|
|
|
+ tk.Button(frame, text="Начать использование",
|
|
|
+ font=self.heading_font,
|
|
|
+ bg=self.colors['accent'], fg=self.colors['text'],
|
|
|
+ command=welcome_window.destroy,
|
|
|
+ padx=30, pady=10, relief='flat').pack()
|
|
|
+
|
|
|
+ def show_error_screen(self, message):
|
|
|
+ """Показывает экран ошибки"""
|
|
|
+ error_frame = tk.Frame(self.root, bg=self.colors['primary'])
|
|
|
+ error_frame.pack(expand=True, fill='both')
|
|
|
+
|
|
|
+ tk.Label(error_frame, text="⚠️", font=("Segoe UI", 72),
|
|
|
+ bg=self.colors['primary'], fg=self.colors['warning']).pack(pady=(100, 30))
|
|
|
+
|
|
|
+ tk.Label(error_frame, text="Ошибка", font=self.title_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text']).pack(pady=(0, 20))
|
|
|
+
|
|
|
+ tk.Label(error_frame, text=message, font=self.body_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text']).pack(pady=(0, 30))
|
|
|
+
|
|
|
+ tk.Button(error_frame, text="Выход", font=self.heading_font,
|
|
|
+ bg=self.colors['warning'], fg=self.colors['text'],
|
|
|
+ command=self.root.destroy,
|
|
|
+ padx=30, pady=10, relief='flat').pack()
|
|
|
+
|
|
|
+ def encrypt(self, data):
|
|
|
+ """Шифрует данные"""
|
|
|
+ return self.cipher.encrypt(data.encode()).decode()
|
|
|
+
|
|
|
+ def decrypt(self, encrypted_data):
|
|
|
+ """Расшифровывает данные"""
|
|
|
+ try:
|
|
|
+ return self.cipher.decrypt(encrypted_data.encode()).decode()
|
|
|
+ except:
|
|
|
+ return encrypted_data
|
|
|
+
|
|
|
+ def load_passwords(self):
|
|
|
+ """Загружает пароли из файла"""
|
|
|
+ if os.path.exists(self.filename):
|
|
|
+ try:
|
|
|
+ with open(self.filename, 'r', encoding='utf-8') as f:
|
|
|
+ encrypted_data = json.load(f)
|
|
|
+ decrypted_json = self.decrypt(encrypted_data)
|
|
|
+ return json.loads(decrypted_json)
|
|
|
+ except:
|
|
|
+ return {}
|
|
|
+ return {}
|
|
|
+
|
|
|
+ def save_passwords(self):
|
|
|
+ """Сохраняет пароли в файл"""
|
|
|
+ try:
|
|
|
+ data_json = json.dumps(self.passwords, indent=2, ensure_ascii=False)
|
|
|
+ encrypted_data = self.encrypt(data_json)
|
|
|
+
|
|
|
+ with open(self.filename, 'w', encoding='utf-8') as f:
|
|
|
+ json.dump(encrypted_data, f, ensure_ascii=False, indent=2)
|
|
|
+
|
|
|
+ self.create_backup()
|
|
|
+ return True
|
|
|
+ except Exception as e:
|
|
|
+ messagebox.showerror("Ошибка", f"Не удалось сохранить данные: {str(e)}")
|
|
|
+ return False
|
|
|
+
|
|
|
+ def create_backup(self):
|
|
|
+ """Создает резервную копию"""
|
|
|
+ try:
|
|
|
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
+ backup_file = os.path.join(self.backup_dir, f"backup_{timestamp}.enc")
|
|
|
+
|
|
|
+ with open(backup_file, 'w', encoding='utf-8') as f:
|
|
|
+ json.dump(self.passwords, f, ensure_ascii=False, indent=2)
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ def export_to_txt(self):
|
|
|
+ """Экспортирует все пароли в текстовый файл"""
|
|
|
+ if not self.passwords:
|
|
|
+ messagebox.showinfo("Информация", "Нет данных для экспорта")
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
+ export_file = os.path.join(self.documents_path, f"passwords_export_{timestamp}.txt")
|
|
|
+
|
|
|
+ with open(export_file, 'w', encoding='utf-8') as f:
|
|
|
+ f.write("=" * 70 + "\n")
|
|
|
+ f.write(" " * 20 + "ЭКСПОРТ ПАРОЛЕЙ\n")
|
|
|
+ f.write("=" * 70 + "\n")
|
|
|
+ f.write(f"Дата: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}\n")
|
|
|
+ f.write(f"Всего записей: {len(self.passwords)}\n")
|
|
|
+ f.write("=" * 70 + "\n\n")
|
|
|
+
|
|
|
+ for i, (service, data) in enumerate(sorted(self.passwords.items()), 1):
|
|
|
+ f.write(f"🔐 ЗАПИСЬ #{i}\n")
|
|
|
+ f.write("-" * 40 + "\n")
|
|
|
+ f.write(f"Сервис: {service}\n")
|
|
|
+ f.write(f"Логин: {data['username']}\n")
|
|
|
+ f.write(f"Пароль: {self.decrypt(data['password'])}\n")
|
|
|
+ if data['notes']:
|
|
|
+ f.write(f"Заметки: {data['notes']}\n")
|
|
|
+ f.write("\n" + "=" * 40 + "\n\n")
|
|
|
+
|
|
|
+ f.write("\n" + "!" * 70 + "\n")
|
|
|
+ f.write("ВНИМАНИЕ: Этот файл содержит пароли в открытом виде!\n")
|
|
|
+ f.write("Храните его в безопасном месте и удалите после использования.\n")
|
|
|
+ f.write("!" * 70 + "\n")
|
|
|
+
|
|
|
+ messagebox.showinfo("Успех",
|
|
|
+ f"✅ Экспорт завершен!\n\n"
|
|
|
+ f"Файл сохранен: {export_file}\n\n"
|
|
|
+ f"⚠️ Файл содержит пароли в открытом виде!\n"
|
|
|
+ f"Рекомендуется удалить его после использования.")
|
|
|
+
|
|
|
+ # Открываем файл
|
|
|
+ if platform.system() == "Windows":
|
|
|
+ os.startfile(export_file)
|
|
|
+ elif platform.system() == "Darwin":
|
|
|
+ os.system(f"open '{export_file}'")
|
|
|
+ else:
|
|
|
+ os.system(f"xdg-open '{export_file}'")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ messagebox.showerror("Ошибка", f"Не удалось экспортировать: {str(e)}")
|
|
|
+
|
|
|
+ def create_main_layout(self):
|
|
|
+ """Создает основной интерфейс"""
|
|
|
+ # Верхняя панель
|
|
|
+ self.create_header()
|
|
|
+
|
|
|
+ # Основное содержание
|
|
|
+ main_container = tk.Frame(self.root, bg=self.colors['secondary'])
|
|
|
+ main_container.pack(fill='both', expand=True, padx=20, pady=10)
|
|
|
+
|
|
|
+ # Левая панель - список
|
|
|
+ left_panel = tk.Frame(main_container, bg=self.colors['primary'])
|
|
|
+ left_panel.pack(side='left', fill='both', expand=True, padx=(0, 10))
|
|
|
+
|
|
|
+ # Правая панель - редактор
|
|
|
+ right_panel = tk.Frame(main_container, bg=self.colors['primary'])
|
|
|
+ right_panel.pack(side='right', fill='both', expand=True)
|
|
|
+
|
|
|
+ # Создаем левую и правую панели
|
|
|
+ self.create_left_panel(left_panel)
|
|
|
+ self.create_right_panel(right_panel)
|
|
|
+
|
|
|
+ # Нижняя панель
|
|
|
+ self.create_footer()
|
|
|
+
|
|
|
+ def create_header(self):
|
|
|
+ """Создает верхнюю панель"""
|
|
|
+ header = tk.Frame(self.root, bg=self.colors['primary'], height=80)
|
|
|
+ header.pack(fill='x', padx=20, pady=(20, 0))
|
|
|
+ header.pack_propagate(False)
|
|
|
+
|
|
|
+ # Логотип и название
|
|
|
+ logo_frame = tk.Frame(header, bg=self.colors['primary'])
|
|
|
+ logo_frame.pack(side='left', padx=20)
|
|
|
+
|
|
|
+ tk.Label(logo_frame, text="🔐", font=("Segoe UI", 32),
|
|
|
+ bg=self.colors['primary'], fg=self.colors['accent']).pack(side='left')
|
|
|
+
|
|
|
+ tk.Label(logo_frame, text="SecurePass", font=self.title_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text']).pack(side='left', padx=(10, 0))
|
|
|
+ tk.Label(logo_frame, text="Manager", font=("Segoe UI", 24, "bold"),
|
|
|
+ bg=self.colors['primary'], fg=self.colors['success']).pack(side='left')
|
|
|
+
|
|
|
+ # Поиск
|
|
|
+ search_frame = tk.Frame(header, bg=self.colors['entry_bg'])
|
|
|
+ search_frame.pack(side='right', padx=20)
|
|
|
+
|
|
|
+ search_icon = tk.Label(search_frame, text="🔍", font=self.body_font,
|
|
|
+ bg=self.colors['entry_bg'], fg=self.colors['text'])
|
|
|
+ search_icon.pack(side='left', padx=(10, 5))
|
|
|
+
|
|
|
+ self.search_var = tk.StringVar()
|
|
|
+ self.search_entry = tk.Entry(search_frame, textvariable=self.search_var,
|
|
|
+ bg=self.colors['entry_bg'], fg=self.colors['text'],
|
|
|
+ insertbackground=self.colors['text'],
|
|
|
+ relief='flat', font=self.body_font)
|
|
|
+ self.search_entry.pack(side='left', fill='x', expand=True, padx=(0, 10), pady=10, ipady=5)
|
|
|
+ self.search_entry.bind('<KeyRelease>', self.on_search)
|
|
|
+
|
|
|
+ # Подсказка
|
|
|
+ self.search_entry.insert(0, "Поиск...")
|
|
|
+ self.search_entry.config(fg='#888888')
|
|
|
+
|
|
|
+ def on_focus_in(event):
|
|
|
+ if self.search_entry.get() == "Поиск...":
|
|
|
+ self.search_entry.delete(0, tk.END)
|
|
|
+ self.search_entry.config(fg=self.colors['text'])
|
|
|
+
|
|
|
+ def on_focus_out(event):
|
|
|
+ if not self.search_entry.get():
|
|
|
+ self.search_entry.insert(0, "Поиск...")
|
|
|
+ self.search_entry.config(fg='#888888')
|
|
|
+
|
|
|
+ self.search_entry.bind('<FocusIn>', on_focus_in)
|
|
|
+ self.search_entry.bind('<FocusOut>', on_focus_out)
|
|
|
+
|
|
|
+ def create_left_panel(self, parent):
|
|
|
+ """Создает левую панель со списком сервисов"""
|
|
|
+ # Заголовок
|
|
|
+ title_frame = tk.Frame(parent, bg=self.colors['primary'])
|
|
|
+ title_frame.pack(fill='x', padx=20, pady=(20, 10))
|
|
|
+
|
|
|
+ tk.Label(title_frame, text="Ваши сервисы", font=self.heading_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text']).pack(side='left')
|
|
|
+
|
|
|
+ count_frame = tk.Frame(title_frame, bg=self.colors['accent'],
|
|
|
+ relief='flat', bd=0)
|
|
|
+ count_frame.pack(side='right', padx=(10, 0))
|
|
|
+ self.count_label = tk.Label(count_frame, text="0", font=("Segoe UI", 10, "bold"),
|
|
|
+ bg=self.colors['accent'], fg=self.colors['text'],
|
|
|
+ padx=8, pady=2)
|
|
|
+ self.count_label.pack()
|
|
|
+
|
|
|
+ # Список с прокруткой
|
|
|
+ list_frame = tk.Frame(parent, bg=self.colors['primary'])
|
|
|
+ list_frame.pack(fill='both', expand=True, padx=20, pady=(0, 20))
|
|
|
+
|
|
|
+ # Создаем Canvas и Scrollbar
|
|
|
+ canvas = tk.Canvas(list_frame, bg=self.colors['primary'],
|
|
|
+ highlightthickness=0)
|
|
|
+ scrollbar = ttk.Scrollbar(list_frame, orient="vertical",
|
|
|
+ command=canvas.yview)
|
|
|
+ self.scrollable_frame = tk.Frame(canvas, bg=self.colors['primary'])
|
|
|
+
|
|
|
+ self.scrollable_frame.bind(
|
|
|
+ "<Configure>",
|
|
|
+ lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
|
+ )
|
|
|
+
|
|
|
+ canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
|
|
+ canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
+
|
|
|
+ # Упаковываем
|
|
|
+ canvas.pack(side="left", fill="both", expand=True)
|
|
|
+ scrollbar.pack(side="right", fill="y")
|
|
|
+
|
|
|
+ # Кнопки действий
|
|
|
+ button_frame = tk.Frame(parent, bg=self.colors['primary'])
|
|
|
+ button_frame.pack(fill='x', padx=20, pady=(0, 20))
|
|
|
+
|
|
|
+ actions = [
|
|
|
+ ("➕ Добавить", self.add_password, self.colors['success']),
|
|
|
+ ("✏️ Редактировать", self.update_password, self.colors['accent']),
|
|
|
+ ("🗑️ Удалить", self.delete_password, self.colors['warning'])
|
|
|
+ ]
|
|
|
+
|
|
|
+ for text, command, color in actions:
|
|
|
+ btn = tk.Button(button_frame, text=text, font=self.body_font,
|
|
|
+ bg=color, fg=self.colors['text'],
|
|
|
+ command=command, relief='flat',
|
|
|
+ padx=20, pady=10)
|
|
|
+ btn.pack(side='left', expand=True, fill='x', padx=(0, 10))
|
|
|
+ btn.bind("<Enter>", lambda e, b=btn: b.config(bg=self.colors['hover']))
|
|
|
+ btn.bind("<Leave>", lambda e, b=btn, c=color: b.config(bg=c))
|
|
|
+
|
|
|
+ def create_right_panel(self, parent):
|
|
|
+ """Создает правую панель с редактором"""
|
|
|
+ # Заголовок
|
|
|
+ title_frame = tk.Frame(parent, bg=self.colors['primary'])
|
|
|
+ title_frame.pack(fill='x', padx=20, pady=(20, 10))
|
|
|
+
|
|
|
+ self.editor_title = tk.Label(title_frame, text="Новая запись",
|
|
|
+ font=self.heading_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text'])
|
|
|
+ self.editor_title.pack(side='left')
|
|
|
+
|
|
|
+ # Карточка формы
|
|
|
+ form_card = tk.Frame(parent, bg=self.colors['card_bg'],
|
|
|
+ relief='flat', bd=0)
|
|
|
+ form_card.pack(fill='both', expand=True, padx=20, pady=(0, 20))
|
|
|
+
|
|
|
+ # Поля формы
|
|
|
+ fields = [
|
|
|
+ ("🌐 Сервис/Сайт", "service"),
|
|
|
+ ("👤 Логин/Email", "username"),
|
|
|
+ ("🔑 Пароль", "password"),
|
|
|
+ ("📝 Заметки", "notes")
|
|
|
+ ]
|
|
|
+
|
|
|
+ self.entries = {}
|
|
|
+
|
|
|
+ for i, (label_text, field_name) in enumerate(fields):
|
|
|
+ frame = tk.Frame(form_card, bg=self.colors['card_bg'])
|
|
|
+ frame.pack(fill='x', padx=30, pady=15)
|
|
|
+
|
|
|
+ # Метка
|
|
|
+ tk.Label(frame, text=label_text, font=self.body_font,
|
|
|
+ bg=self.colors['card_bg'], fg=self.colors['text'],
|
|
|
+ width=15, anchor='w').pack(side='left')
|
|
|
+
|
|
|
+ if field_name == "notes":
|
|
|
+ # Многострочное поле для заметок
|
|
|
+ entry_frame = tk.Frame(frame, bg=self.colors['entry_bg'])
|
|
|
+ entry_frame.pack(side='left', fill='both', expand=True, padx=(10, 0))
|
|
|
+
|
|
|
+ entry = scrolledtext.ScrolledText(entry_frame,
|
|
|
+ bg=self.colors['entry_bg'],
|
|
|
+ fg=self.colors['text'],
|
|
|
+ insertbackground=self.colors['text'],
|
|
|
+ font=self.body_font,
|
|
|
+ relief='flat',
|
|
|
+ height=4)
|
|
|
+ entry.pack(fill='both', expand=True, padx=5, pady=5)
|
|
|
+ elif field_name == "password":
|
|
|
+ # Поле пароля с кнопками
|
|
|
+ entry_frame = tk.Frame(frame, bg=self.colors['entry_bg'])
|
|
|
+ entry_frame.pack(side='left', fill='x', expand=True, padx=(10, 0))
|
|
|
+
|
|
|
+ self.password_var = tk.StringVar()
|
|
|
+ entry = tk.Entry(entry_frame, textvariable=self.password_var,
|
|
|
+ bg=self.colors['entry_bg'], fg=self.colors['text'],
|
|
|
+ insertbackground=self.colors['text'],
|
|
|
+ font=self.mono_font,
|
|
|
+ relief='flat', show="●")
|
|
|
+ entry.pack(side='left', fill='x', expand=True, padx=(5, 0), pady=5, ipady=5)
|
|
|
+
|
|
|
+ # Кнопки управления паролем
|
|
|
+ btn_frame = tk.Frame(entry_frame, bg=self.colors['entry_bg'])
|
|
|
+ btn_frame.pack(side='right', padx=(5, 5))
|
|
|
+
|
|
|
+ buttons = [
|
|
|
+ ("👁", self.toggle_password_visibility),
|
|
|
+ ("📋", self.copy_password),
|
|
|
+ ("🎲", self.generate_password)
|
|
|
+ ]
|
|
|
+
|
|
|
+ for btn_text, btn_command in buttons:
|
|
|
+ btn = tk.Button(btn_frame, text=btn_text, font=self.body_font,
|
|
|
+ bg=self.colors['secondary'], fg=self.colors['text'],
|
|
|
+ command=btn_command, relief='flat',
|
|
|
+ width=3)
|
|
|
+ btn.pack(side='left', padx=2)
|
|
|
+ btn.bind("<Enter>", lambda e, b=btn: b.config(bg=self.colors['hover']))
|
|
|
+ btn.bind("<Leave>", lambda e, b=btn: b.config(bg=self.colors['secondary']))
|
|
|
+ else:
|
|
|
+ # Обычное поле ввода
|
|
|
+ entry_frame = tk.Frame(frame, bg=self.colors['entry_bg'])
|
|
|
+ entry_frame.pack(side='left', fill='x', expand=True, padx=(10, 0))
|
|
|
+
|
|
|
+ entry = tk.Entry(entry_frame, bg=self.colors['entry_bg'],
|
|
|
+ fg=self.colors['text'],
|
|
|
+ insertbackground=self.colors['text'],
|
|
|
+ font=self.body_font,
|
|
|
+ relief='flat')
|
|
|
+ entry.pack(fill='x', expand=True, padx=5, pady=5, ipady=5)
|
|
|
+
|
|
|
+ self.entries[field_name] = entry
|
|
|
+
|
|
|
+ # Кнопки сохранения
|
|
|
+ button_frame = tk.Frame(form_card, bg=self.colors['card_bg'])
|
|
|
+ button_frame.pack(fill='x', padx=30, pady=(20, 30))
|
|
|
+
|
|
|
+ save_btn = tk.Button(button_frame, text="💾 Сохранить",
|
|
|
+ font=self.heading_font,
|
|
|
+ bg=self.colors['success'], fg=self.colors['text'],
|
|
|
+ command=self.save_current, relief='flat',
|
|
|
+ padx=40, pady=12)
|
|
|
+ save_btn.pack(side='left')
|
|
|
+ save_btn.bind("<Enter>", lambda e: save_btn.config(bg=self.colors['hover']))
|
|
|
+ save_btn.bind("<Leave>", lambda e: save_btn.config(bg=self.colors['success']))
|
|
|
+
|
|
|
+ clear_btn = tk.Button(button_frame, text="🔄 Очистить",
|
|
|
+ font=self.heading_font,
|
|
|
+ bg=self.colors['secondary'], fg=self.colors['text'],
|
|
|
+ command=self.clear_form, relief='flat',
|
|
|
+ padx=40, pady=12)
|
|
|
+ clear_btn.pack(side='left', padx=(20, 0))
|
|
|
+ clear_btn.bind("<Enter>", lambda e: clear_btn.config(bg=self.colors['hover']))
|
|
|
+ clear_btn.bind("<Leave>", lambda e: clear_btn.config(bg=self.colors['secondary']))
|
|
|
+
|
|
|
+ def create_footer(self):
|
|
|
+ """Создает нижнюю панель"""
|
|
|
+ footer = tk.Frame(self.root, bg=self.colors['primary'], height=60)
|
|
|
+ footer.pack(fill='x', side='bottom', padx=20, pady=(0, 20))
|
|
|
+ footer.pack_propagate(False)
|
|
|
+
|
|
|
+ # Левая часть - информация
|
|
|
+ left_info = tk.Frame(footer, bg=self.colors['primary'])
|
|
|
+ left_info.pack(side='left', padx=20)
|
|
|
+
|
|
|
+ stats_text = f"🛡️ Защищено записей: {len(self.passwords)}"
|
|
|
+ self.stats_label = tk.Label(left_info, text=stats_text, font=self.body_font,
|
|
|
+ bg=self.colors['primary'], fg=self.colors['text'])
|
|
|
+ self.stats_label.pack(side='left')
|
|
|
+
|
|
|
+ # Правая часть - кнопки
|
|
|
+ right_buttons = tk.Frame(footer, bg=self.colors['primary'])
|
|
|
+ right_buttons.pack(side='right', padx=20)
|
|
|
+
|
|
|
+ export_btn = tk.Button(right_buttons, text="📄 Экспорт в TXT",
|
|
|
+ font=self.body_font,
|
|
|
+ bg=self.colors['accent'], fg=self.colors['text'],
|
|
|
+ command=self.export_to_txt, relief='flat',
|
|
|
+ padx=20, pady=8)
|
|
|
+ export_btn.pack(side='left', padx=(0, 10))
|
|
|
+ export_btn.bind("<Enter>", lambda e: export_btn.config(bg=self.colors['hover']))
|
|
|
+ export_btn.bind("<Leave>", lambda e: export_btn.config(bg=self.colors['accent']))
|
|
|
+
|
|
|
+ backup_btn = tk.Button(right_buttons, text="💾 Создать резервную копию",
|
|
|
+ font=self.body_font,
|
|
|
+ bg=self.colors['secondary'], fg=self.colors['text'],
|
|
|
+ command=self.create_manual_backup, relief='flat',
|
|
|
+ padx=20, pady=8)
|
|
|
+ backup_btn.pack(side='left')
|
|
|
+ backup_btn.bind("<Enter>", lambda e: backup_btn.config(bg=self.colors['hover']))
|
|
|
+ backup_btn.bind("<Leave>", lambda e: backup_btn.config(bg=self.colors['secondary']))
|
|
|
+
|
|
|
+ def create_manual_backup(self):
|
|
|
+ """Создает ручную резервную копию"""
|
|
|
+ try:
|
|
|
+ self.save_passwords()
|
|
|
+ messagebox.showinfo("Успех", "✅ Резервная копия создана!")
|
|
|
+ except:
|
|
|
+ messagebox.showerror("Ошибка", "Не удалось создать резервную копию")
|
|
|
+
|
|
|
+ def update_service_list(self, search_term=None):
|
|
|
+ """Обновляет список сервисов"""
|
|
|
+ # Очищаем текущий список
|
|
|
+ for widget in self.scrollable_frame.winfo_children():
|
|
|
+ widget.destroy()
|
|
|
+
|
|
|
+ services = list(self.passwords.keys())
|
|
|
+ if search_term and search_term != "Поиск...":
|
|
|
+ search_term = search_term.lower()
|
|
|
+ services = [s for s in services if search_term in s.lower()]
|
|
|
+
|
|
|
+ self.count_label.config(text=str(len(services)))
|
|
|
+ self.stats_label.config(text=f"🛡️ Защищено записей: {len(self.passwords)}")
|
|
|
+
|
|
|
+ if not services:
|
|
|
+ empty_label = tk.Label(self.scrollable_frame,
|
|
|
+ text="Нет сохраненных сервисов",
|
|
|
+ font=self.body_font,
|
|
|
+ bg=self.colors['primary'], fg='#888888',
|
|
|
+ pady=20)
|
|
|
+ empty_label.pack()
|
|
|
+ return
|
|
|
+
|
|
|
+ for service in sorted(services):
|
|
|
+ self.create_service_card(service)
|
|
|
+
|
|
|
+ def create_service_card(self, service):
|
|
|
+ """Создает карточку сервиса"""
|
|
|
+ card = tk.Frame(self.scrollable_frame, bg=self.colors['card_bg'],
|
|
|
+ relief='flat', bd=0)
|
|
|
+ card.pack(fill='x', pady=5, padx=5)
|
|
|
+
|
|
|
+ # Основное содержимое карточки
|
|
|
+ content_frame = tk.Frame(card, bg=self.colors['card_bg'])
|
|
|
+ content_frame.pack(fill='x', padx=15, pady=10)
|
|
|
+
|
|
|
+ # Иконка (первая буква сервиса)
|
|
|
+ icon_frame = tk.Frame(content_frame, bg=self.colors['accent'],
|
|
|
+ width=40, height=40)
|
|
|
+ icon_frame.pack(side='left')
|
|
|
+ icon_frame.pack_propagate(False)
|
|
|
+
|
|
|
+ icon_label = tk.Label(icon_frame, text=service[0].upper(),
|
|
|
+ font=("Segoe UI", 16, "bold"),
|
|
|
+ bg=self.colors['accent'], fg=self.colors['text'])
|
|
|
+ icon_label.pack(expand=True)
|
|
|
+
|
|
|
+ # Информация
|
|
|
+ info_frame = tk.Frame(content_frame, bg=self.colors['card_bg'])
|
|
|
+ info_frame.pack(side='left', fill='x', expand=True, padx=(15, 0))
|
|
|
+
|
|
|
+ # Название сервиса
|
|
|
+ name_label = tk.Label(info_frame, text=service,
|
|
|
+ font=self.heading_font,
|
|
|
+ bg=self.colors['card_bg'], fg=self.colors['text'],
|
|
|
+ anchor='w')
|
|
|
+ name_label.pack(fill='x')
|
|
|
+
|
|
|
+ # Логин
|
|
|
+ if service in self.passwords:
|
|
|
+ username = self.passwords[service]['username']
|
|
|
+ user_label = tk.Label(info_frame, text=f"👤 {username}",
|
|
|
+ font=self.body_font,
|
|
|
+ bg=self.colors['card_bg'], fg='#AAAAAA',
|
|
|
+ anchor='w')
|
|
|
+ user_label.pack(fill='x', pady=(2, 0))
|
|
|
+
|
|
|
+ # Привязываем клик
|
|
|
+ card.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+ content_frame.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+ icon_frame.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+ info_frame.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+ name_label.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+ if 'user_label' in locals():
|
|
|
+ user_label.bind("<Button-1>", lambda e, s=service: self.select_service(s))
|
|
|
+
|
|
|
+ # Эффект при наведении
|
|
|
+ def on_enter(e):
|
|
|
+ card.config(bg=self.colors['hover'])
|
|
|
+ content_frame.config(bg=self.colors['hover'])
|
|
|
+ info_frame.config(bg=self.colors['hover'])
|
|
|
+ name_label.config(bg=self.colors['hover'])
|
|
|
+ if 'user_label' in locals():
|
|
|
+ user_label.config(bg=self.colors['hover'])
|
|
|
+
|
|
|
+ def on_leave(e):
|
|
|
+ card.config(bg=self.colors['card_bg'])
|
|
|
+ content_frame.config(bg=self.colors['card_bg'])
|
|
|
+ info_frame.config(bg=self.colors['card_bg'])
|
|
|
+ name_label.config(bg=self.colors['card_bg'])
|
|
|
+ if 'user_label' in locals():
|
|
|
+ user_label.config(bg=self.colors['card_bg'])
|
|
|
+
|
|
|
+ card.bind("<Enter>", on_enter)
|
|
|
+ card.bind("<Leave>", on_leave)
|
|
|
+ content_frame.bind("<Enter>", on_enter)
|
|
|
+ content_frame.bind("<Leave>", on_leave)
|
|
|
+
|
|
|
+ def select_service(self, service):
|
|
|
+ """Выбирает сервис из списка"""
|
|
|
+ if service in self.passwords:
|
|
|
+ data = self.passwords[service]
|
|
|
+
|
|
|
+ # Обновляем заголовок
|
|
|
+ self.editor_title.config(text=f"Редактирование: {service}")
|
|
|
+
|
|
|
+ # Заполняем поля
|
|
|
+ for widget in self.scrollable_frame.winfo_children():
|
|
|
+ if hasattr(widget, 'selected'):
|
|
|
+ widget.config(bg=self.colors['card_bg'])
|
|
|
+
|
|
|
+ self.entries['service'].delete(0, tk.END)
|
|
|
+ self.entries['service'].insert(0, service)
|
|
|
+
|
|
|
+ self.entries['username'].delete(0, tk.END)
|
|
|
+ self.entries['username'].insert(0, data['username'])
|
|
|
+
|
|
|
+ password = self.decrypt(data['password'])
|
|
|
+ self.password_var.set(password)
|
|
|
+
|
|
|
+ self.entries['notes'].delete('1.0', tk.END)
|
|
|
+ self.entries['notes'].insert('1.0', data['notes'])
|
|
|
+
|
|
|
+ def on_search(self, event=None):
|
|
|
+ """Обрабатывает поиск"""
|
|
|
+ search_term = self.search_var.get()
|
|
|
+ if search_term == "Поиск...":
|
|
|
+ search_term = ""
|
|
|
+ self.update_service_list(search_term)
|
|
|
+
|
|
|
+ def add_password(self):
|
|
|
+ """Добавляет новую запись"""
|
|
|
+ self.clear_form()
|
|
|
+ self.editor_title.config(text="Новая запись")
|
|
|
+ self.entries['service'].focus()
|
|
|
+
|
|
|
+ def clear_form(self):
|
|
|
+ """Очищает форму"""
|
|
|
+ self.editor_title.config(text="Новая запись")
|
|
|
+ self.entries['service'].delete(0, tk.END)
|
|
|
+ self.entries['username'].delete(0, tk.END)
|
|
|
+ self.password_var.set("")
|
|
|
+ self.entries['notes'].delete('1.0', tk.END)
|
|
|
+
|
|
|
+ # Сбрасываем выделение в списке
|
|
|
+ for widget in self.scrollable_frame.winfo_children():
|
|
|
+ if hasattr(widget, 'selected'):
|
|
|
+ widget.config(bg=self.colors['card_bg'])
|
|
|
+
|
|
|
+ def save_current(self):
|
|
|
+ """Сохраняет текущую запись"""
|
|
|
+ service = self.entries['service'].get().strip()
|
|
|
+ username = self.entries['username'].get().strip()
|
|
|
+ password = self.password_var.get().strip()
|
|
|
+
|
|
|
+ if not service:
|
|
|
+ messagebox.showerror("Ошибка", "Введите название сервиса")
|
|
|
+ self.entries['service'].focus()
|
|
|
+ return
|
|
|
+ if not username:
|
|
|
+ messagebox.showerror("Ошибка", "Введите логин или email")
|
|
|
+ self.entries['username'].focus()
|
|
|
+ return
|
|
|
+ if not password:
|
|
|
+ messagebox.showerror("Ошибка", "Введите пароль")
|
|
|
+ self.entries['password'].focus()
|
|
|
+ return
|
|
|
+
|
|
|
+ notes = self.entries['notes'].get('1.0', tk.END).strip()
|
|
|
+
|
|
|
+ # Шифруем пароль
|
|
|
+ encrypted_password = self.encrypt(password)
|
|
|
+
|
|
|
+ self.passwords[service] = {
|
|
|
+ 'username': username,
|
|
|
+ 'password': encrypted_password,
|
|
|
+ 'notes': notes
|
|
|
+ }
|
|
|
+
|
|
|
+ if self.save_passwords():
|
|
|
+ self.update_service_list(self.search_var.get() if self.search_var.get() != "Поиск..." else None)
|
|
|
+
|
|
|
+ # Показываем анимацию успеха
|
|
|
+ self.show_success_animation("Запись сохранена!")
|
|
|
+
|
|
|
+ def update_password(self):
|
|
|
+ """Обновляет выбранную запись"""
|
|
|
+ service = self.entries['service'].get().strip()
|
|
|
+ if not service or service not in self.passwords:
|
|
|
+ messagebox.showwarning("Внимание", "Выберите запись для редактирования")
|
|
|
+ return
|
|
|
+
|
|
|
+ self.save_current()
|
|
|
+
|
|
|
+ def delete_password(self):
|
|
|
+ """Удаляет выбранную запись"""
|
|
|
+ service = self.entries['service'].get().strip()
|
|
|
+ if not service or service not in self.passwords:
|
|
|
+ messagebox.showwarning("Внимание", "Выберите запись для удаления")
|
|
|
+ return
|
|
|
+
|
|
|
+ if messagebox.askyesno("Подтверждение",
|
|
|
+ f"Вы уверены, что хотите удалить запись '{service}'?",
|
|
|
+ icon='warning'):
|
|
|
+ del self.passwords[service]
|
|
|
+ self.save_passwords()
|
|
|
+ self.update_service_list(self.search_var.get() if self.search_var.get() != "Поиск..." else None)
|
|
|
+ self.clear_form()
|
|
|
+ self.show_success_animation("Запись удалена!")
|
|
|
+
|
|
|
+ def toggle_password_visibility(self):
|
|
|
+ """Показывает/скрывает пароль"""
|
|
|
+ current_state = self.entries['password'].cget('show')
|
|
|
+ if current_state == "●":
|
|
|
+ self.entries['password'].config(show="")
|
|
|
+ else:
|
|
|
+ self.entries['password'].config(show="●")
|
|
|
+
|
|
|
+ def copy_password(self):
|
|
|
+ """Копирует пароль в буфер обмена"""
|
|
|
+ password = self.password_var.get()
|
|
|
+ if password:
|
|
|
+ pyperclip.copy(password)
|
|
|
+ self.show_temp_message("Пароль скопирован!")
|
|
|
+
|
|
|
+ def generate_password(self):
|
|
|
+ """Генерирует случайный пароль"""
|
|
|
+ length = 16
|
|
|
+ chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
|
+ password = ''.join(random.choice(chars) for _ in range(length))
|
|
|
+ self.password_var.set(password)
|
|
|
+ self.show_temp_message("Пароль сгенерирован!")
|
|
|
+
|
|
|
+ def show_success_animation(self, message):
|
|
|
+ """Показывает анимацию успеха"""
|
|
|
+ # Можно добавить более сложную анимацию
|
|
|
+ messagebox.showinfo("Успех", f"✅ {message}")
|
|
|
+
|
|
|
+ def show_temp_message(self, message):
|
|
|
+ """Показывает временное сообщение"""
|
|
|
+ # Можно реализовать тост-уведомление
|
|
|
+ print(f"📢 {message}")
|
|
|
+
|
|
|
+ def animate_welcome(self):
|
|
|
+ """Простая анимация приветствия"""
|
|
|
+ # Можно добавить анимацию появления элементов
|
|
|
+ pass
|
|
|
+
|
|
|
+def main():
|
|
|
+ root = tk.Tk()
|
|
|
+
|
|
|
+ # Проверяем библиотеки
|
|
|
+ try:
|
|
|
+ import cryptography
|
|
|
+ import pyperclip
|
|
|
+ except ImportError:
|
|
|
+ # Простое окно с ошибкой
|
|
|
+ error_root = tk.Tk()
|
|
|
+ error_root.title("Ошибка")
|
|
|
+ error_root.geometry("400x200")
|
|
|
+
|
|
|
+ tk.Label(error_root, text="❌ Необходимо установить библиотеки:",
|
|
|
+ font=("Segoe UI", 14)).pack(pady=20)
|
|
|
+ tk.Label(error_root, text="pip install cryptography pyperclip",
|
|
|
+ font=("Consolas", 12)).pack(pady=10)
|
|
|
+ tk.Button(error_root, text="Выход", command=error_root.destroy,
|
|
|
+ padx=20, pady=10).pack(pady=20)
|
|
|
+
|
|
|
+ error_root.mainloop()
|
|
|
+ return
|
|
|
+
|
|
|
+ # Запускаем приложение
|
|
|
+ app = ModernPasswordManager(root)
|
|
|
+ root.mainloop()
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|