commit a385308f38b09664b7ddb6ce3cbc8e17de614447 Author: Vic Sergeev Date: Sat Jul 4 16:04:48 2026 +0300 working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/CapLog.iml b/.idea/CapLog.iml new file mode 100644 index 0000000..f2cca7e --- /dev/null +++ b/.idea/CapLog.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..25d64c7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2089c61 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b71b4f8 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,3 @@ +""" +Captain's Log - Приложение для быстрых заметок +""" \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..70737b6 Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..dff823c Binary files /dev/null and b/app/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/__pycache__/auth.cpython-312.pyc b/app/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..ae177e6 Binary files /dev/null and b/app/__pycache__/auth.cpython-312.pyc differ diff --git a/app/__pycache__/auth.cpython-314.pyc b/app/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000..eb6fa0f Binary files /dev/null and b/app/__pycache__/auth.cpython-314.pyc differ diff --git a/app/__pycache__/config.cpython-312.pyc b/app/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..2d5b78f Binary files /dev/null and b/app/__pycache__/config.cpython-312.pyc differ diff --git a/app/__pycache__/config.cpython-314.pyc b/app/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..b4e1387 Binary files /dev/null and b/app/__pycache__/config.cpython-314.pyc differ diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..02bb234 Binary files /dev/null and b/app/__pycache__/database.cpython-312.pyc differ diff --git a/app/__pycache__/database.cpython-314.pyc b/app/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000..b3d88c3 Binary files /dev/null and b/app/__pycache__/database.cpython-314.pyc differ diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..ef5e3ed --- /dev/null +++ b/app/auth.py @@ -0,0 +1,39 @@ +"""Модуль аутентификации""" +import hashlib +import base64 +from app.database import Database + + +class Auth: + def __init__(self, db: Database): + self.db = db + + def set_master_password(self, password): + """Установить мастер-пароль""" + password_hash = self._hash_password(password) + self.db.set_setting('master_password_hash', password_hash) + self.db.set_setting('password_set', '1') + + def check_password(self, password): + """Проверить мастер-пароль""" + stored_hash = self.db.get_setting('master_password_hash') + if not stored_hash: + return True # Пароль не установлен + + return self._hash_password(password) == stored_hash + + def is_password_set(self): + """Проверяет, установлен ли пароль""" + return self.db.get_setting('password_set', '0') == '1' + + def ask_password_on_startup(self): + """Спрашивать ли пароль при входе""" + return self.db.get_setting('ask_password_on_startup', '1') == '1' + + def set_ask_password_on_startup(self, value): + """Установить, спрашивать ли пароль при входе""" + self.db.set_setting('ask_password_on_startup', '1' if value else '0') + + def _hash_password(self, password): + """Хешировать пароль""" + return hashlib.sha256(password.encode()).hexdigest() \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..f0f6899 --- /dev/null +++ b/app/config.py @@ -0,0 +1,51 @@ +"""Конфигурация приложения""" +import os +import sys + + +class Config: + APP_NAME = "Captain's Log" + APP_VERSION = "0.1.0" + + # Путь к директории приложения + if getattr(sys, 'frozen', False): + APP_DIR = os.path.dirname(sys.executable) + else: + APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # База данных + DB_NAME = "captains_log.db" + DB_PATH = os.path.join(APP_DIR, DB_NAME) + + # Настройки UI + WINDOW_WIDTH = 900 + WINDOW_HEIGHT = 650 + SIDEBAR_WIDTH = 350 + MIN_WINDOW_WIDTH = 800 + MIN_WINDOW_HEIGHT = 600 + + # Настройки цветов + COLORS = { + 'bg': '#2b2b2b', + 'fg': '#ffffff', + 'sidebar_bg': '#333333', + 'editor_bg': '#3c3c3c', + 'button_bg': '#4a4a4a', + 'button_fg': '#ffffff', + 'button_hover': '#5a5a5a', + 'accent': '#007acc', + 'accent_hover': '#005f9e', + 'danger': '#d9534f', + 'danger_hover': '#c9302c', + 'success': '#5cb85c', + 'today_bg': '#007acc', + 'text_bg': '#252525', + 'text_fg': '#ffffff', + 'calendar_bg': '#333333', + 'calendar_fg': '#ffffff', + 'calendar_header': '#007acc', + 'list_bg': '#2d2d2d', + 'list_fg': '#ffffff', + 'list_selected': '#404040', + 'separator': '#555555' + } \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..c98bc33 --- /dev/null +++ b/app/database.py @@ -0,0 +1,245 @@ +"""Модуль для работы с базой данных SQLite""" +import sqlite3 +import os +from datetime import datetime +from app.config import Config + + +class Database: + def __init__(self): + self.db_path = Config.DB_PATH + self.init_db() + + def get_connection(self): + """Получить соединение с БД""" + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + return conn + + def init_db(self): + """Инициализация базы данных""" + with self.get_connection() as conn: + cursor = conn.cursor() + + # Таблица настроек + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings + ( + key + TEXT + PRIMARY + KEY, + value + TEXT + NOT + NULL + ) + ''') + + # Таблица заметок + cursor.execute(''' + CREATE TABLE IF NOT EXISTS notes + ( + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, + title + TEXT, + content + TEXT, + created_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, + updated_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + + def get_setting(self, key, default=None): + """Получить значение настройки""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT value FROM settings WHERE key = ?', (key,)) + row = cursor.fetchone() + return row['value'] if row else default + + def set_setting(self, key, value): + """Установить значение настройки""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO settings (key, value) + VALUES (?, ?) + ''', (key, value)) + conn.commit() + + def get_notes_by_date(self, year, month, day=None): + """Получить заметки по дате""" + with self.get_connection() as conn: + cursor = conn.cursor() + + if day: + cursor.execute(''' + SELECT * + FROM notes + WHERE strftime('%Y', created_at) = ? + AND strftime('%m', created_at) = ? + AND strftime('%d', created_at) = ? + ORDER BY created_at DESC + ''', (str(year), str(month).zfill(2), str(day).zfill(2))) + else: + cursor.execute(''' + SELECT * + FROM notes + WHERE strftime('%Y', created_at) = ? + AND strftime('%m', created_at) = ? + ORDER BY created_at DESC + ''', (str(year), str(month).zfill(2))) + + return [dict(row) for row in cursor.fetchall()] + + def get_notes_with_content_by_date(self, date_str): + """Получить заметки с контентом по дате (YYYY-MM-DD)""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT * + FROM notes + WHERE date (created_at) = ? + ORDER BY created_at DESC + ''', (date_str,)) + return [dict(row) for row in cursor.fetchall()] + + def get_note_by_id(self, note_id): + """Получить заметку по ID""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM notes WHERE id = ?', (note_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + def create_note(self, title=None, content='', created_at=None): + """Создать новую заметку""" + with self.get_connection() as conn: + cursor = conn.cursor() + + if created_at: + cursor.execute(''' + INSERT INTO notes (title, content, created_at, updated_at) + VALUES (?, ?, ?, ?) + ''', (title, content, created_at, created_at)) + else: + cursor.execute(''' + INSERT INTO notes (title, content) + VALUES (?, ?) + ''', (title, content)) + + conn.commit() + return cursor.lastrowid + + def update_note(self, note_id, title=None, content=None): + """Обновить заметку""" + with self.get_connection() as conn: + cursor = conn.cursor() + + updates = [] + params = [] + + if title is not None: + updates.append('title = ?') + params.append(title) + + if content is not None: + updates.append('content = ?') + params.append(content) + + if updates: + updates.append('updated_at = CURRENT_TIMESTAMP') + params.append(note_id) + + cursor.execute(f''' + UPDATE notes + SET {', '.join(updates)} + WHERE id = ? + ''', params) + conn.commit() + + def delete_note(self, note_id): + """Удалить заметку""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM notes WHERE id = ?', (note_id,)) + conn.commit() + + def get_all_notes(self): + """Получить все заметки""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM notes ORDER BY created_at DESC') + return [dict(row) for row in cursor.fetchall()] + + def export_data(self): + """Экспортировать все данные""" + return { + 'notes': self.get_all_notes(), + 'settings': self.get_all_settings() + } + + def get_all_settings(self): + """Получить все настройки""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM settings') + return {row['key']: row['value'] for row in cursor.fetchall()} + + def import_data(self, data): + """Импортировать данные""" + with self.get_connection() as conn: + cursor = conn.cursor() + + # Очищаем существующие данные + cursor.execute('DELETE FROM notes') + cursor.execute('DELETE FROM settings') + + # Импортируем заметки + for note in data.get('notes', []): + cursor.execute(''' + INSERT INTO notes (id, title, content, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + ''', ( + note['id'], + note.get('title'), + note.get('content', ''), + note.get('created_at'), + note.get('updated_at') + )) + + # Импортируем настройки + for key, value in data.get('settings', {}).items(): + cursor.execute(''' + INSERT INTO settings (key, value) + VALUES (?, ?) + ''', (key, value)) + + conn.commit() + + def get_dates_with_notes(self, year, month): + """Получить дни месяца, в которых есть заметки""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT DISTINCT strftime('%d', created_at) as day + FROM notes + WHERE strftime('%Y' + , created_at) = ? + AND strftime('%m' + , created_at) = ? + ''', (str(year), str(month).zfill(2))) + return [int(row['day']) for row in cursor.fetchall()] \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..e69de29 diff --git a/app/ui/__init__.py b/app/ui/__init__.py new file mode 100644 index 0000000..7cff528 --- /dev/null +++ b/app/ui/__init__.py @@ -0,0 +1 @@ +"""UI модули""" \ No newline at end of file diff --git a/app/ui/__pycache__/__init__.cpython-312.pyc b/app/ui/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1797d36 Binary files /dev/null and b/app/ui/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/ui/__pycache__/__init__.cpython-314.pyc b/app/ui/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..9f8842e Binary files /dev/null and b/app/ui/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/ui/__pycache__/calendar_widget.cpython-312.pyc b/app/ui/__pycache__/calendar_widget.cpython-312.pyc new file mode 100644 index 0000000..647fc23 Binary files /dev/null and b/app/ui/__pycache__/calendar_widget.cpython-312.pyc differ diff --git a/app/ui/__pycache__/calendar_widget.cpython-314.pyc b/app/ui/__pycache__/calendar_widget.cpython-314.pyc new file mode 100644 index 0000000..4727a79 Binary files /dev/null and b/app/ui/__pycache__/calendar_widget.cpython-314.pyc differ diff --git a/app/ui/__pycache__/dialogs.cpython-312.pyc b/app/ui/__pycache__/dialogs.cpython-312.pyc new file mode 100644 index 0000000..4ef234e Binary files /dev/null and b/app/ui/__pycache__/dialogs.cpython-312.pyc differ diff --git a/app/ui/__pycache__/dialogs.cpython-314.pyc b/app/ui/__pycache__/dialogs.cpython-314.pyc new file mode 100644 index 0000000..643cfa6 Binary files /dev/null and b/app/ui/__pycache__/dialogs.cpython-314.pyc differ diff --git a/app/ui/__pycache__/main_window.cpython-312.pyc b/app/ui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000..7db96ac Binary files /dev/null and b/app/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/app/ui/__pycache__/main_window.cpython-314.pyc b/app/ui/__pycache__/main_window.cpython-314.pyc new file mode 100644 index 0000000..81a947d Binary files /dev/null and b/app/ui/__pycache__/main_window.cpython-314.pyc differ diff --git a/app/ui/__pycache__/note_editor.cpython-312.pyc b/app/ui/__pycache__/note_editor.cpython-312.pyc new file mode 100644 index 0000000..5fae9bd Binary files /dev/null and b/app/ui/__pycache__/note_editor.cpython-312.pyc differ diff --git a/app/ui/__pycache__/note_editor.cpython-314.pyc b/app/ui/__pycache__/note_editor.cpython-314.pyc new file mode 100644 index 0000000..cea8586 Binary files /dev/null and b/app/ui/__pycache__/note_editor.cpython-314.pyc differ diff --git a/app/ui/__pycache__/notes_list.cpython-312.pyc b/app/ui/__pycache__/notes_list.cpython-312.pyc new file mode 100644 index 0000000..eb06c1a Binary files /dev/null and b/app/ui/__pycache__/notes_list.cpython-312.pyc differ diff --git a/app/ui/__pycache__/notes_list.cpython-314.pyc b/app/ui/__pycache__/notes_list.cpython-314.pyc new file mode 100644 index 0000000..9ea1fbe Binary files /dev/null and b/app/ui/__pycache__/notes_list.cpython-314.pyc differ diff --git a/app/ui/__pycache__/sidebar.cpython-312.pyc b/app/ui/__pycache__/sidebar.cpython-312.pyc new file mode 100644 index 0000000..be12058 Binary files /dev/null and b/app/ui/__pycache__/sidebar.cpython-312.pyc differ diff --git a/app/ui/__pycache__/sidebar.cpython-314.pyc b/app/ui/__pycache__/sidebar.cpython-314.pyc new file mode 100644 index 0000000..8598f3a Binary files /dev/null and b/app/ui/__pycache__/sidebar.cpython-314.pyc differ diff --git a/app/ui/calendar_widget.py b/app/ui/calendar_widget.py new file mode 100644 index 0000000..2eb5e2c --- /dev/null +++ b/app/ui/calendar_widget.py @@ -0,0 +1,189 @@ +"""Виджет календаря""" +import tkinter as tk +from tkinter import ttk +import calendar +from datetime import datetime +from app.config import Config + + +class CalendarWidget(tk.Frame): + """Кастомный виджет календаря""" + + def __init__(self, parent, db, on_date_select=None): + super().__init__(parent, bg=Config.COLORS['calendar_bg']) + self.db = db + self.on_date_select = on_date_select + + self.current_date = datetime.now() + self.selected_date = datetime.now() + + self.create_widgets() + self.update_calendar() + + def create_widgets(self): + # Заголовок с месяцем и годом + self.header_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + self.header_frame.pack(fill='x', pady=(10, 5)) + + self.prev_btn = tk.Button( + self.header_frame, + text="◀", + command=self.prev_month, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 12), + cursor='hand2' + ) + self.prev_btn.pack(side='left', padx=(20, 0)) + + self.month_label = tk.Label( + self.header_frame, + text="", + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['calendar_header'], + font=('Arial', 14, 'bold') + ) + self.month_label.pack(side='left', expand=True) + + self.next_btn = tk.Button( + self.header_frame, + text="▶", + command=self.next_month, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 12), + cursor='hand2' + ) + self.next_btn.pack(side='right', padx=(0, 20)) + + # Дни недели + days_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + days_frame.pack(fill='x', padx=5, pady=5) + + days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] + for i, day in enumerate(days): + days_frame.grid_columnconfigure(i, weight=1) + tk.Label( + days_frame, + text=day, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 9, 'bold') + ).grid(row=0, column=i) + + # Сетка календаря + self.calendar_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + self.calendar_frame.pack(fill='both', expand=True, padx=5, pady=5) + + self.day_buttons = [] + for row in range(6): + self.calendar_frame.grid_rowconfigure(row, weight=1) + for col in range(7): + self.calendar_frame.grid_columnconfigure(col, weight=1) + btn = tk.Label( + self.calendar_frame, + text="", + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + cursor='hand2' + ) + btn.grid(row=row, column=col, sticky='nsew', padx=1, pady=1) + btn.bind('', lambda e, r=row, c=col: self.on_day_click(r, c)) + self.day_buttons.append(btn) + + def update_calendar(self): + year = self.current_date.year + month = self.current_date.month + + # Обновляем заголовок + months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', + 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'] + self.month_label.config(text=f"{months[month - 1]} {year}") + + # Получаем дни с заметками + days_with_notes = self.db.get_dates_with_notes(year, month) + + # Создаем календарь + cal = calendar.monthcalendar(year, month) + + for idx, btn in enumerate(self.day_buttons): + row = idx // 7 + col = idx % 7 + + if row < len(cal) and col < len(cal[row]): + day = cal[row][col] + if day != 0: + btn.config(text=str(day)) + + # Определяем цвет фона + today = datetime.now() + is_today = (day == today.day and + month == today.month and + year == today.year) + + is_selected = (day == self.selected_date.day and + month == self.selected_date.month and + year == self.selected_date.year) + + has_notes = day in days_with_notes + + if is_today and is_selected: + btn.config( + bg=Config.COLORS['today_bg'], + fg=Config.COLORS['fg'] + ) + elif is_today: + btn.config( + bg=Config.COLORS['today_bg'], + fg=Config.COLORS['fg'] + ) + elif is_selected: + btn.config( + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'] + ) + elif has_notes: + btn.config( + bg='#5a7a3a', + fg=Config.COLORS['fg'] + ) + else: + btn.config( + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'] + ) + else: + btn.config(text="", bg=Config.COLORS['calendar_bg']) + else: + btn.config(text="", bg=Config.COLORS['calendar_bg']) + + def prev_month(self): + if self.current_date.month == 1: + self.current_date = self.current_date.replace(year=self.current_date.year - 1, month=12) + else: + self.current_date = self.current_date.replace(month=self.current_date.month - 1) + self.update_calendar() + + def next_month(self): + if self.current_date.month == 12: + self.current_date = self.current_date.replace(year=self.current_date.year + 1, month=1) + else: + self.current_date = self.current_date.replace(month=self.current_date.month + 1) + self.update_calendar() + + def on_day_click(self, row, col): + year = self.current_date.year + month = self.current_date.month + cal = calendar.monthcalendar(year, month) + + if row < len(cal) and col < len(cal[row]): + day = cal[row][col] + if day != 0: + self.selected_date = datetime(year, month, day) + self.update_calendar() + + if self.on_date_select: + self.on_date_select(self.selected_date) \ No newline at end of file diff --git a/app/ui/dialogs.py b/app/ui/dialogs.py new file mode 100644 index 0000000..0664ee2 --- /dev/null +++ b/app/ui/dialogs.py @@ -0,0 +1,219 @@ +"""Диалоговые окна""" +import tkinter as tk +from tkinter import ttk, messagebox +from app.config import Config + + +class PasswordDialog(tk.Toplevel): + """Диалог установки/ввода мастер-пароля""" + + def __init__(self, parent, auth, mode='set'): + super().__init__(parent) + self.auth = auth + self.mode = mode # 'set' или 'check' + self.result = False + + self.title("Мастер-пароль" if mode == 'set' else "Введите пароль") + self.geometry("400x250") + self.resizable(False, False) + self.configure(bg=Config.COLORS['bg']) + + self.transient(parent) + self.grab_set() + + self.create_widgets() + + # Центрируем окно + self.update_idletasks() + x = parent.winfo_rootx() + (parent.winfo_width() - 400) // 2 + y = parent.winfo_rooty() + (parent.winfo_height() - 250) // 2 + self.geometry(f"+{x}+{y}") + + def create_widgets(self): + main_frame = tk.Frame(self, bg=Config.COLORS['bg']) + main_frame.pack(expand=True, fill='both', padx=20, pady=20) + + if self.mode == 'set': + text = "Придумайте мастер-пароль\nдля защиты ваших заметок:" + else: + text = "Введите мастер-пароль:" + + tk.Label( + main_frame, + text=text, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 12), + wraplength=350 + ).pack(pady=(0, 15)) + + # Поле ввода пароля + self.password_var = tk.StringVar() + self.password_entry = ttk.Entry( + main_frame, + textvariable=self.password_var, + show='*', + font=('Arial', 12) + ) + self.password_entry.pack(fill='x', pady=(0, 10)) + self.password_entry.focus() + + if self.mode == 'set': + # Подтверждение пароля + tk.Label( + main_frame, + text="Подтвердите пароль:", + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11) + ).pack() + + self.confirm_var = tk.StringVar() + self.confirm_entry = ttk.Entry( + main_frame, + textvariable=self.confirm_var, + show='*', + font=('Arial', 12) + ) + self.confirm_entry.pack(fill='x', pady=(0, 15)) + + # Чекбокс "спрашивать при входе" + self.ask_var = tk.BooleanVar(value=True) + tk.Checkbutton( + main_frame, + text="Спрашивать при входе", + variable=self.ask_var, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + selectcolor=Config.COLORS['bg'], + activebackground=Config.COLORS['bg'], + activeforeground=Config.COLORS['fg'] + ).pack(pady=(0, 15)) + + # Кнопки + button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg']) + button_frame.pack(fill='x') + + if self.mode == 'set': + tk.Button( + button_frame, + text="Установить пароль", + command=self.set_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + else: + tk.Button( + button_frame, + text="Войти", + command=self.check_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + + if self.mode == 'check': + tk.Button( + button_frame, + text="Отмена", + command=self.destroy, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2' + ).pack(fill='x') + + def set_password(self): + password = self.password_var.get() + confirm = self.confirm_var.get() + + if not password: + messagebox.showwarning("Ошибка", "Введите пароль") + return + + if password != confirm: + messagebox.showwarning("Ошибка", "Пароли не совпадают") + return + + self.auth.set_master_password(password) + self.auth.set_ask_password_on_startup(self.ask_var.get()) + self.result = True + self.destroy() + + def check_password(self): + password = self.password_var.get() + + if self.auth.check_password(password): + self.result = True + self.destroy() + else: + messagebox.showerror("Ошибка", "Неверный пароль") + self.password_var.set('') + self.password_entry.focus() + + +class ConfirmDialog(tk.Toplevel): + """Диалог подтверждения действия""" + + def __init__(self, parent, title, message): + super().__init__(parent) + self.result = False + + self.title(title) + self.geometry("350x150") + self.resizable(False, False) + self.configure(bg=Config.COLORS['bg']) + + self.transient(parent) + self.grab_set() + + # Центрируем окно + self.update_idletasks() + x = parent.winfo_rootx() + (parent.winfo_width() - 350) // 2 + y = parent.winfo_rooty() + (parent.winfo_height() - 150) // 2 + self.geometry(f"+{x}+{y}") + + # Сообщение + tk.Label( + self, + text=message, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11), + wraplength=320 + ).pack(expand=True, pady=(20, 10)) + + # Кнопки + btn_frame = tk.Frame(self, bg=Config.COLORS['bg']) + btn_frame.pack(fill='x', padx=20, pady=(0, 20)) + + tk.Button( + btn_frame, + text="Да", + command=self.confirm, + bg=Config.COLORS['danger'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2', + width=10 + ).pack(side='left', padx=(0, 10)) + + tk.Button( + btn_frame, + text="Нет", + command=self.destroy, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2', + width=10 + ).pack(side='left') + + def confirm(self): + self.result = True + self.destroy() \ No newline at end of file diff --git a/app/ui/main_window.py b/app/ui/main_window.py new file mode 100644 index 0000000..4c5fb5f --- /dev/null +++ b/app/ui/main_window.py @@ -0,0 +1,212 @@ +"""Главное окно приложения""" +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import json +import zipfile +import os +from datetime import datetime +from app.config import Config +from app.database import Database +from app.auth import Auth +from app.ui.sidebar import Sidebar +from app.ui.note_editor import NoteEditor +from app.ui.dialogs import PasswordDialog + + +class MainWindow(tk.Tk): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + + self.title(Config.APP_NAME) + self.geometry(f"{Config.WINDOW_WIDTH}x{Config.WINDOW_HEIGHT}") + self.minsize(Config.MIN_WINDOW_WIDTH, Config.MIN_WINDOW_HEIGHT) + self.configure(bg=Config.COLORS['bg']) + + # Инициализация компонентов + self.db = Database() + self.auth = Auth(self.db) + + # Проверка пароля + if not self.check_auth(): + self.destroy() + return + + # Создание интерфейса + self.create_menu() + self.create_widgets() + + # Загрузка заметок для текущей даты + self.sidebar.refresh_notes() + + # Привязка закрытия окна + self.protocol("WM_DELETE_WINDOW", self.on_closing) + + def check_auth(self): + """Проверка аутентификации""" + if self.auth.is_password_set(): + if self.auth.ask_password_on_startup(): + dialog = PasswordDialog(self, self.auth, mode='check') + self.wait_window(dialog) + return dialog.result + else: + # Предлагаем создать пароль + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + return dialog.result + + return True + + def create_menu(self): + """Создание меню""" + menubar = tk.Menu(self) + self.config(menu=menubar) + + # Меню Файл + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Файл", menu=file_menu) + + file_menu.add_command(label="Экспорт в ZIP", command=self.export_to_zip, accelerator="Ctrl+E") + file_menu.add_command(label="Импорт из ZIP", command=self.import_from_zip, accelerator="Ctrl+I") + file_menu.add_separator() + file_menu.add_command(label="Выход", command=self.on_closing, accelerator="Ctrl+Q") + + # Меню Настройки + settings_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Настройки", menu=settings_menu) + + settings_menu.add_command(label="Изменить мастер-пароль", command=self.change_password) + + # Горячие клавиши + self.bind('', lambda e: self.export_to_zip()) + self.bind('', lambda e: self.import_from_zip()) + self.bind('', lambda e: self.on_closing()) + + def create_widgets(self): + """Создание виджетов интерфейса""" + # Основной контейнер + main_container = tk.Frame(self, bg=Config.COLORS['bg']) + main_container.pack(fill='both', expand=True) + + # Боковая панель + self.sidebar = Sidebar( + main_container, + self.db, + on_note_select=self.on_note_selected, + on_note_delete=self.on_note_deleted + ) + + # Разделитель + separator = tk.Frame(main_container, width=2, bg=Config.COLORS['separator']) + separator.pack(side='left', fill='y') + + # Редактор заметок + self.editor = NoteEditor( + main_container, + self.db, + on_note_saved=self.on_note_saved, + on_note_deleted=self.on_note_deleted, + on_new_note=self.on_new_note_created + ) + self.editor.pack(side='left', fill='both', expand=True) + + def on_note_selected(self, note): + """Обработчик выбора заметки""" + self.editor.show_note(note) + + def on_note_saved(self, note): + """Обработчик сохранения заметки""" + self.sidebar.refresh_notes() + + def on_note_deleted(self): + """Обработчик удаления заметки""" + self.sidebar.refresh_notes() + + def on_new_note_created(self): + """Обработчик создания новой заметки""" + pass + + def export_to_zip(self): + """Экспорт данных в ZIP архив""" + filename = filedialog.asksaveasfilename( + defaultextension=".zip", + filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")], + initialfile=f"captains_log_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" + ) + + if filename: + try: + # Экспортируем данные + data = self.db.export_data() + + # Создаем временный JSON файл + temp_json = os.path.join(os.path.dirname(filename), 'temp_export.json') + with open(temp_json, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + # Создаем ZIP архив + with zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + zipf.write(temp_json, 'captains_log_backup.json') + + # Удаляем временный файл + os.remove(temp_json) + + messagebox.showinfo("Успех", f"Данные экспортированы в:\n{filename}") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось экспортировать данные:\n{str(e)}") + + def import_from_zip(self): + """Импорт данных из ZIP архива""" + filename = filedialog.askopenfilename( + filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")] + ) + + if filename: + # Подтверждение + if not messagebox.askyesno( + "Подтверждение", + "Импорт данных заменит все существующие заметки.\nПродолжить?" + ): + return + + try: + # Извлекаем из ZIP + with zipfile.ZipFile(filename, 'r') as zipf: + temp_json = os.path.join(os.path.dirname(filename), 'temp_import.json') + zipf.extract('captains_log_backup.json', os.path.dirname(filename)) + + # Читаем JSON + with open(temp_json, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Импортируем данные + self.db.import_data(data) + + # Удаляем временный файл + os.remove(temp_json) + + # Обновляем интерфейс + self.sidebar.refresh_notes() + self.editor.show_empty_state() + + messagebox.showinfo("Успех", "Данные успешно импортированы") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось импортировать данные:\n{str(e)}") + + def change_password(self): + """Изменение мастер-пароля""" + if self.auth.is_password_set(): + dialog = PasswordDialog(self, self.auth, mode='check') + self.wait_window(dialog) + + if dialog.result: + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + else: + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + + def on_closing(self): + """Обработчик закрытия окна""" + self.destroy() \ No newline at end of file diff --git a/app/ui/note_editor.py b/app/ui/note_editor.py new file mode 100644 index 0000000..c4ba58b --- /dev/null +++ b/app/ui/note_editor.py @@ -0,0 +1,297 @@ +"""Редактор заметок""" +import tkinter as tk +from tkinter import ttk, messagebox +from datetime import datetime +from app.config import Config +from app.ui.dialogs import ConfirmDialog + +class NoteEditor(tk.Frame): + """Правая панель с редактором заметок""" + + def __init__(self, parent, db, on_note_saved=None, on_note_deleted=None, on_new_note=None): + super().__init__(parent, bg=Config.COLORS['editor_bg']) + self.db = db + self.on_note_saved = on_note_saved + self.on_note_deleted = on_note_deleted + self.on_new_note = on_new_note + + self.current_note = None + self.is_new_note = False + self.is_editing = False + + self.create_widgets() + self.show_empty_state() + + def create_widgets(self): + # Панель инструментов + self.toolbar = tk.Frame(self, bg=Config.COLORS['sidebar_bg'], height=40) + self.toolbar.pack(fill='x') + self.toolbar.pack_propagate(False) + + # Дата и время (левый край) + self.date_label = tk.Label( + self.toolbar, + text="", + bg=Config.COLORS['sidebar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11) + ) + self.date_label.pack(side='left', padx=10) + + # Кнопки (правый край) + buttons_frame = tk.Frame(self.toolbar, bg=Config.COLORS['sidebar_bg']) + buttons_frame.pack(side='right', padx=5) + + # Кнопка сохранения + self.save_btn = tk.Button( + buttons_frame, + text="💾", + command=self.save_note, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + self.save_btn.pack(side='left', padx=2) + + # Кнопка удаления + self.delete_btn = tk.Button( + buttons_frame, + text="🗑", + command=self.delete_current_note, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + self.delete_btn.pack(side='left', padx=2) + + # Кнопка создания новой заметки + self.new_btn = tk.Button( + buttons_frame, + text="+", + command=self.create_new_note, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14, 'bold'), + cursor='hand2', + width=2 + ) + self.new_btn.pack(side='left', padx=2) + + # Скрываем кнопки сохранения и удаления по умолчанию + self.hide_editor_buttons() + + # Область контента + self.content_frame = tk.Frame(self, bg=Config.COLORS['editor_bg']) + self.content_frame.pack(fill='both', expand=True, padx=10, pady=10) + + # Текстовое поле для редактирования + self.text_widget = tk.Text( + self.content_frame, + bg=Config.COLORS['text_bg'], + fg=Config.COLORS['text_fg'], + font=('Arial', 12), + wrap='word', + relief='flat', + padx=10, + pady=10, + insertbackground=Config.COLORS['fg'] + ) + + # Scrollbar для текстового поля + self.text_scrollbar = ttk.Scrollbar( + self.content_frame, + orient='vertical', + command=self.text_widget.yview + ) + self.text_widget.configure(yscrollcommand=self.text_scrollbar.set) + + # Метка для пустого состояния + self.empty_label = tk.Label( + self.content_frame, + text="Выберите заметку из списка\nили создайте новую", + bg=Config.COLORS['editor_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 14), + justify='center' + ) + self.empty_label.pack(expand=True) + + # Привязываем события изменения текста + self.text_widget.bind('<>', self.on_text_modified) + + def show_editor_buttons(self): + """Показать кнопки редактора""" + self.save_btn.pack(side='left', padx=2) + self.delete_btn.pack(side='left', padx=2) + + def hide_editor_buttons(self): + """Скрыть кнопки редактора""" + self.save_btn.pack_forget() + self.delete_btn.pack_forget() + + def show_empty_state(self): + """Показать пустое состояние""" + self.text_widget.pack_forget() + self.text_scrollbar.pack_forget() + self.empty_label.pack(expand=True) + self.date_label.config(text="") + self.hide_editor_buttons() + self.current_note = None + self.is_new_note = False + self.is_editing = False + + def show_note(self, note): + """Показать существующую заметку""" + self.current_note = note + self.is_new_note = False + self.is_editing = False + + # Скрываем пустую метку + self.empty_label.pack_forget() + + # Показываем текстовое поле + self.text_widget.pack(side='left', fill='both', expand=True) + self.text_scrollbar.pack(side='right', fill='y') + + # Устанавливаем дату + created_at = datetime.strptime( + note['created_at'], '%Y-%m-%d %H:%M:%S' + ) + self.date_label.config( + text=created_at.strftime('%d.%m.%Y %H:%M') + ) + + # Очищаем и вставляем текст + self.text_widget.delete('1.0', 'end') + self.text_widget.insert('1.0', note.get('content', '')) + self.text_widget.edit_modified(False) + + # Показываем кнопки + self.show_editor_buttons() + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + + # Привязываем клик для редактирования + self.text_widget.bind('', self.enable_editing) + + def enable_editing(self, event=None): + """Включить режим редактирования""" + if not self.is_editing and self.current_note: + self.is_editing = True + self.text_widget.configure(state='normal') + self.text_widget.unbind('') + + def create_new_note(self): + """Создать новую заметку""" + self.current_note = None + self.is_new_note = True + self.is_editing = True + + # Скрываем пустую метку + self.empty_label.pack_forget() + + # Показываем текстовое поле + self.text_widget.pack(side='left', fill='both', expand=True) + self.text_scrollbar.pack(side='right', fill='y') + + # Устанавливаем текущую дату + now = datetime.now() + self.date_label.config(text=now.strftime('%d.%m.%Y %H:%M')) + + # Очищаем текстовое поле + self.text_widget.delete('1.0', 'end') + self.text_widget.configure(state='normal') + self.text_widget.edit_modified(False) + + # Показываем кнопки + self.show_editor_buttons() + + # Убираем привязку клика + self.text_widget.unbind('') + + # Ставим фокус + self.text_widget.focus_set() + + if self.on_new_note: + self.on_new_note() + + def save_note(self): + """Сохранить заметку""" + content = self.text_widget.get('1.0', 'end-1c') + + if not content.strip() and self.is_new_note: + messagebox.showwarning( + "Предупреждение", + "Нельзя сохранить пустую заметку" + ) + return + + if self.is_new_note: + # Создаем новую заметку + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + note_id = self.db.create_note( + title=None, + content=content, + created_at=now + ) + self.current_note = self.db.get_note_by_id(note_id) + self.is_new_note = False + self.is_editing = False + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + self.text_widget.bind('', self.enable_editing) + + messagebox.showinfo("Успех", "Заметка сохранена") + else: + # Обновляем существующую заметку + self.db.update_note( + self.current_note['id'], + content=content + ) + self.current_note = self.db.get_note_by_id(self.current_note['id']) + self.is_editing = False + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + self.text_widget.bind('', self.enable_editing) + + messagebox.showinfo("Успех", "Заметка обновлена") + + self.text_widget.edit_modified(False) + + if self.on_note_saved: + self.on_note_saved(self.current_note) + + def delete_current_note(self): + """Удалить текущую заметку""" + if not self.current_note: + return + + dialog = ConfirmDialog( + self.winfo_toplevel(), + "Удаление заметки", + "Вы уверены, что хотите удалить эту заметку?" + ) + self.wait_window(dialog) + + if dialog.result: + self.db.delete_note(self.current_note['id']) + self.show_empty_state() + + if self.on_note_deleted: + self.on_note_deleted() + + def on_text_modified(self, event=None): + """Обработчик изменения текста""" + if self.text_widget.edit_modified() and self.current_note and not self.is_editing: + # Текст был изменен программно, игнорируем + self.text_widget.edit_modified(False) \ No newline at end of file diff --git a/app/ui/notes_list.py b/app/ui/notes_list.py new file mode 100644 index 0000000..ba72a82 --- /dev/null +++ b/app/ui/notes_list.py @@ -0,0 +1,230 @@ +"""Список заметок""" +import tkinter as tk +from tkinter import ttk +from datetime import datetime +from app.config import Config +from app.ui.dialogs import ConfirmDialog + + +class NotesList(tk.Frame): + """Виджет списка заметок""" + + def __init__(self, parent, db, on_note_select=None, on_note_delete=None): + super().__init__(parent, bg=Config.COLORS['list_bg']) + self.db = db + self.on_note_select = on_note_select + self.on_note_delete = on_note_delete + + self.notes = [] + self.selected_note_id = None + + self.create_widgets() + + def create_widgets(self): + # Заголовок + self.header_label = tk.Label( + self, + text="Заметки", + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 12, 'bold') + ) + self.header_label.pack(fill='x', padx=10, pady=(10, 5)) + + # Разделитель + separator = tk.Frame(self, height=1, bg=Config.COLORS['separator']) + separator.pack(fill='x', padx=5) + + # Canvas и Scrollbar для списка + self.canvas = tk.Canvas( + self, + bg=Config.COLORS['list_bg'], + highlightthickness=0 + ) + self.scrollbar = ttk.Scrollbar( + self, + orient="vertical", + command=self.canvas.yview + ) + + self.notes_frame = tk.Frame(self.canvas, bg=Config.COLORS['list_bg']) + + self.notes_frame.bind("", + lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) + + self.canvas_window = self.canvas.create_window( + (0, 0), window=self.notes_frame, anchor="nw" + ) + + self.canvas.configure(yscrollcommand=self.scrollbar.set) + + self.canvas.pack(side="left", fill="both", expand=True) + self.scrollbar.pack(side="right", fill="y") + + # Bind для изменения размера + self.canvas.bind('', self.on_canvas_configure) + + # Bind для колесика мыши + self.canvas.bind_all("", self.on_mousewheel) + + def on_canvas_configure(self, event): + self.canvas.itemconfig(self.canvas_window, width=event.width) + + def on_mousewheel(self, event): + self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def load_notes(self, date): + """Загрузить заметки для выбранной даты""" + # Очищаем текущий список + for widget in self.notes_frame.winfo_children(): + widget.destroy() + + date_str = date.strftime('%Y-%m-%d') + self.notes = self.db.get_notes_with_content_by_date(date_str) + + if not self.notes: + empty_label = tk.Label( + self.notes_frame, + text="Нет заметок\nна этот день", + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + justify='center' + ) + empty_label.pack(expand=True, pady=20) + return + + for note in self.notes: + self.create_note_item(note) + + def create_note_item(self, note): + """Создать элемент списка для заметки""" + item_frame = tk.Frame( + self.notes_frame, + bg=Config.COLORS['list_bg'], + cursor='hand2' + ) + item_frame.pack(fill='x', padx=5, pady=2) + + # Рамка для элемента + content_frame = tk.Frame( + item_frame, + bg=Config.COLORS['list_bg'], + highlightbackground=Config.COLORS['separator'], + highlightthickness=1 + ) + content_frame.pack(fill='x', padx=5, pady=2) + + # Левая часть с датой + date_frame = tk.Frame(content_frame, bg=Config.COLORS['list_bg']) + date_frame.pack(side='left', padx=(10, 5), pady=5) + + # Получаем дату заметки + created_at = datetime.strptime( + note['created_at'], '%Y-%m-%d %H:%M:%S' + ) + + day_month = created_at.strftime('%d.%m') + year = created_at.strftime('%Y') + + tk.Label( + date_frame, + text=day_month, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold') + ).pack(anchor='w') + + tk.Label( + date_frame, + text=year, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 9) + ).pack(anchor='w') + + # Средняя часть с заголовком + title_frame = tk.Frame(content_frame, bg=Config.COLORS['list_bg']) + title_frame.pack(side='left', fill='x', expand=True, padx=10, pady=5) + + title = note.get('title', '') + if not title: + # Берем первые 22 символа текста + content = note.get('content', '') + title = content[:22] + ('...' if len(content) > 22 else '') + if not title: + title = "Пустая заметка" + + title_label = tk.Label( + title_frame, + text=title, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + anchor='w', + justify='left' + ) + title_label.pack(anchor='w') + + # Кнопка удаления + delete_btn = tk.Button( + content_frame, + text="🗑", + command=lambda n=note: self.delete_note(n), + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['danger'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + delete_btn.pack(side='right', padx=(0, 10), pady=5) + + # Привязываем события клика для выбора заметки + for widget in [item_frame, content_frame, date_frame, title_frame, title_label]: + widget.bind('', lambda e, n=note: self.select_note(n)) + + # Подсветка при наведении + for widget in [content_frame, date_frame, title_frame]: + widget.bind('', lambda e, f=content_frame: + f.configure(bg=Config.COLORS['list_selected'])) + widget.bind('', lambda e, f=content_frame: + f.configure(bg=Config.COLORS['list_bg'])) + + def select_note(self, note): + """Выбрать заметку""" + self.selected_note_id = note['id'] + + # Обновляем визуальное выделение + for widget in self.notes_frame.winfo_children(): + if isinstance(widget, tk.Frame): + for child in widget.winfo_children(): + if isinstance(child, tk.Frame): + child.configure(bg=Config.COLORS['list_bg']) + + if self.on_note_select: + self.on_note_select(note) + + def delete_note(self, note): + """Удалить заметку с подтверждением""" + dialog = ConfirmDialog( + self.winfo_toplevel(), + "Удаление заметки", + "Вы уверены, что хотите удалить эту заметку?" + ) + self.wait_window(dialog) + + if dialog.result: + self.db.delete_note(note['id']) + + if self.on_note_delete: + self.on_note_delete() + + # Перезагружаем список + if hasattr(self, 'current_date'): + self.load_notes(self.current_date) + + def refresh(self, date): + """Обновить список заметок""" + self.current_date = date + self.load_notes(date) \ No newline at end of file diff --git a/app/ui/sidebar.py b/app/ui/sidebar.py new file mode 100644 index 0000000..6e2814b --- /dev/null +++ b/app/ui/sidebar.py @@ -0,0 +1,59 @@ +"""Боковая панель""" +import tkinter as tk +from app.config import Config +from app.ui.calendar_widget import CalendarWidget +from app.ui.notes_list import NotesList + + +class Sidebar(tk.Frame): + """Левая боковая панель""" + + def __init__(self, parent, db, on_date_select=None, on_note_select=None, on_note_delete=None): + super().__init__(parent, bg=Config.COLORS['sidebar_bg'], width=Config.SIDEBAR_WIDTH) + self.db = db + self.on_note_select = on_note_select + + self.pack_propagate(False) + self.pack(side='left', fill='y') + + self.create_widgets(on_date_select, on_note_delete) + + def create_widgets(self, on_date_select, on_note_delete): + # Календарь + self.calendar = CalendarWidget( + self, + self.db, + on_date_select=self.on_date_selected + ) + self.calendar.pack(fill='x') + + # Разделитель + separator = tk.Frame(self, height=2, bg=Config.COLORS['separator']) + separator.pack(fill='x', padx=10, pady=(5, 0)) + + # Список заметок + self.notes_list = NotesList( + self, + self.db, + on_note_select=self.on_note_selected, + on_note_delete=on_note_delete + ) + self.notes_list.pack(fill='both', expand=True) + + def on_date_selected(self, date): + """Обработчик выбора даты в календаре""" + self.notes_list.refresh(date) + + def on_note_selected(self, note): + """Обработчик выбора заметки""" + if self.on_note_select: + self.on_note_select(note) + + def refresh_notes(self): + """Обновить список заметок для текущей даты""" + if self.calendar: + self.notes_list.refresh(self.calendar.selected_date) + + def get_selected_date(self): + """Получить выбранную дату""" + return self.calendar.selected_date if self.calendar else None \ No newline at end of file diff --git a/captains_log.db b/captains_log.db new file mode 100644 index 0000000..0b4fd29 Binary files /dev/null and b/captains_log.db differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..b6d6533 --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Captain's Log - Приложение для быстрых заметок +""" + +import sys +import os + +# Добавляем текущую директорию в путь для импорта +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app.ui.main_window import MainWindow + +def main(): + """Точка входа в приложение""" + app = MainWindow() + app.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b819adf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tkcalendar==1.6.1 +nuitka>=2.0.0 \ No newline at end of file