diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8dd4581
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+*.egg-info/
+dist/
+build/
+*.egg
+
+# Виртуальное окружение
+.venv/
+venv/
+ENV/
+env/
+
+# База данных и данные приложения
+captains_log.db
+*.db
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Nuitka
+*.build/
+*.dist/
+*.onefile-build/
+
+# OS
+.DS_Store
+Thumbs.db
\ No newline at end of file
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..25adb28
--- /dev/null
+++ b/app/auth.py
@@ -0,0 +1,55 @@
+"""Модуль аутентификации"""
+import hashlib
+import os
+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 reset_password(self):
+ """Сбросить мастер-пароль"""
+ self.db.set_setting('password_set', '0')
+ self.db.set_setting('master_password_hash', '')
+ self.db.set_setting('ask_password_on_startup', '1')
+
+ def change_password(self, old_password, new_password):
+ """Изменить мастер-пароль"""
+ if self.is_password_set():
+ if not self.check_password(old_password):
+ return False, "Неверный текущий пароль"
+
+ self.set_master_password(new_password)
+ return True, "Пароль успешно изменен"
+
+ def _hash_password(self, password):
+ """Хешировать пароль с солью"""
+ # В будущем здесь будет более безопасное хеширование
+ # Пока используем SHA-256 для простоты
+ 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..50807ce
--- /dev/null
+++ b/app/ui/dialogs.py
@@ -0,0 +1,296 @@
+"""Диалоговые окна"""
+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', 'reset'
+ self.result = False
+
+ self.title("Мастер-пароль" if mode == 'set' else
+ "Введите пароль" if mode == 'check' else
+ "Сброс пароля")
+ self.geometry("400x300" if mode == 'reset' else "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для защиты ваших заметок:"
+ elif self.mode == 'check':
+ text = "Введите мастер-пароль:"
+ else: # reset
+ text = "ВНИМАНИЕ!\nЭто сбросит текущий пароль.\nВсе заметки останутся.\n\nДля сброса введите 'RESET':"
+
+ tk.Label(
+ main_frame,
+ text=text,
+ bg=Config.COLORS['bg'],
+ fg=Config.COLORS['fg'],
+ font=('Arial', 12),
+ wraplength=350
+ ).pack(pady=(0, 15))
+
+ if self.mode == 'reset':
+ # Поле для сброса
+ self.reset_var = tk.StringVar()
+ self.reset_entry = ttk.Entry(
+ main_frame,
+ textvariable=self.reset_var,
+ font=('Arial', 12)
+ )
+ self.reset_entry.pack(fill='x', pady=(0, 15))
+ self.reset_entry.focus()
+
+ tk.Button(
+ main_frame,
+ text="Сбросить пароль",
+ command=self.reset_password,
+ bg=Config.COLORS['danger'],
+ fg=Config.COLORS['fg'],
+ font=('Arial', 11, 'bold'),
+ relief='flat',
+ cursor='hand2'
+ ).pack(fill='x', pady=(0, 5))
+
+ tk.Button(
+ main_frame,
+ text="Отмена",
+ command=self.destroy,
+ bg=Config.COLORS['button_bg'],
+ fg=Config.COLORS['fg'],
+ relief='flat',
+ cursor='hand2'
+ ).pack(fill='x')
+ else:
+ # Поле ввода пароля
+ 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))
+
+ # Кнопка сброса пароля
+ tk.Button(
+ button_frame,
+ text="Забыли пароль?",
+ command=self.show_reset_warning,
+ bg=Config.COLORS['button_bg'],
+ fg=Config.COLORS['fg'],
+ relief='flat',
+ cursor='hand2',
+ font=('Arial', 9)
+ ).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 len(password) < 4:
+ messagebox.showwarning("Ошибка", "Пароль должен быть минимум 4 символа")
+ 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()
+
+ def reset_password(self):
+ if self.reset_var.get().upper() == 'RESET':
+ self.auth.reset_password()
+ messagebox.showinfo("Успех", "Пароль сброшен.\nПри следующем запуске создайте новый пароль.")
+ self.result = True
+ self.destroy()
+ else:
+ messagebox.showerror("Ошибка", "Введите RESET для подтверждения сброса")
+ self.reset_var.set('')
+ self.reset_entry.focus()
+
+ def show_reset_warning(self):
+ """Показать предупреждение о сбросе пароля"""
+ if messagebox.askyesno(
+ "Сброс пароля",
+ "Вы уверены, что хотите сбросить пароль?\n\n"
+ "Это действие удалит только пароль, все заметки останутся.\n"
+ "После сброса вы сможете установить новый пароль."
+ ):
+ self.destroy()
+ reset_dialog = PasswordDialog(self.master, self.auth, mode='reset')
+ self.master.wait_window(reset_dialog)
+ if reset_dialog.result:
+ self.result = True
+ self.destroy()
+
+
+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..361baef
--- /dev/null
+++ b/main.py
@@ -0,0 +1,73 @@
+#!/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 create_gitignore():
+ """Создать .gitignore если его нет"""
+ gitignore_path = os.path.join(os.path.dirname(__file__), '.gitignore')
+
+ if not os.path.exists(gitignore_path):
+ gitignore_content = """# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+*.egg-info/
+dist/
+build/
+*.egg
+
+# Виртуальное окружение
+.venv/
+venv/
+ENV/
+env/
+
+# База данных и данные приложения
+captains_log.db
+*.db
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Nuitka
+*.build/
+*.dist/
+*.onefile-build/
+
+# OS
+.DS_Store
+Thumbs.db
+"""
+ try:
+ with open(gitignore_path, 'w') as f:
+ f.write(gitignore_content)
+ except:
+ pass # Игнорируем ошибки при создании .gitignore
+
+
+def main():
+ """Точка входа в приложение"""
+ # Создаем .gitignore при первом запуске
+ create_gitignore()
+
+ 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