This commit is contained in:
Vic Sergeev 2026-07-04 16:04:48 +03:00
commit a385308f38
44 changed files with 1614 additions and 0 deletions

1
app/ui/__init__.py Normal file
View file

@ -0,0 +1 @@
"""UI модули"""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

189
app/ui/calendar_widget.py Normal file
View file

@ -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('<Button-1>', 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)

219
app/ui/dialogs.py Normal file
View file

@ -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()

212
app/ui/main_window.py Normal file
View file

@ -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('<Control-e>', lambda e: self.export_to_zip())
self.bind('<Control-i>', lambda e: self.import_from_zip())
self.bind('<Control-q>', 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()

297
app/ui/note_editor.py Normal file
View file

@ -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('<<Modified>>', 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('<Button-1>', 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('<Button-1>')
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('<Button-1>')
# Ставим фокус
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('<Button-1>', 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('<Button-1>', 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)

230
app/ui/notes_list.py Normal file
View file

@ -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("<Configure>",
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('<Configure>', self.on_canvas_configure)
# Bind для колесика мыши
self.canvas.bind_all("<MouseWheel>", 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('<Button-1>', lambda e, n=note: self.select_note(n))
# Подсветка при наведении
for widget in [content_frame, date_frame, title_frame]:
widget.bind('<Enter>', lambda e, f=content_frame:
f.configure(bg=Config.COLORS['list_selected']))
widget.bind('<Leave>', 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)

59
app/ui/sidebar.py Normal file
View file

@ -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