working
This commit is contained in:
commit
a385308f38
44 changed files with 1614 additions and 0 deletions
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -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/
|
||||
10
.idea/CapLog.iml
generated
Normal file
10
.idea/CapLog.iml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (CapLog)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (CapLog)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (CapLog)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/CapLog.iml" filepath="$PROJECT_DIR$/.idea/CapLog.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
3
app/__init__.py
Normal file
3
app/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Captain's Log - Приложение для быстрых заметок
|
||||
"""
|
||||
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/auth.cpython-312.pyc
Normal file
BIN
app/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/auth.cpython-314.pyc
Normal file
BIN
app/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-314.pyc
Normal file
BIN
app/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-312.pyc
Normal file
BIN
app/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-314.pyc
Normal file
BIN
app/__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
39
app/auth.py
Normal file
39
app/auth.py
Normal file
|
|
@ -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()
|
||||
51
app/config.py
Normal file
51
app/config.py
Normal file
|
|
@ -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'
|
||||
}
|
||||
245
app/database.py
Normal file
245
app/database.py
Normal file
|
|
@ -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()]
|
||||
0
app/models.py
Normal file
0
app/models.py
Normal file
1
app/ui/__init__.py
Normal file
1
app/ui/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""UI модули"""
|
||||
BIN
app/ui/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/calendar_widget.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/calendar_widget.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/calendar_widget.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/calendar_widget.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/dialogs.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/dialogs.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/dialogs.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/dialogs.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/main_window.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/main_window.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/main_window.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/main_window.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/note_editor.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/note_editor.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/note_editor.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/note_editor.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/notes_list.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/notes_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/notes_list.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/notes_list.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/sidebar.cpython-312.pyc
Normal file
BIN
app/ui/__pycache__/sidebar.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/sidebar.cpython-314.pyc
Normal file
BIN
app/ui/__pycache__/sidebar.cpython-314.pyc
Normal file
Binary file not shown.
189
app/ui/calendar_widget.py
Normal file
189
app/ui/calendar_widget.py
Normal 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
219
app/ui/dialogs.py
Normal 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
212
app/ui/main_window.py
Normal 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
297
app/ui/note_editor.py
Normal 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
230
app/ui/notes_list.py
Normal 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
59
app/ui/sidebar.py
Normal 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
|
||||
BIN
captains_log.db
Normal file
BIN
captains_log.db
Normal file
Binary file not shown.
20
main.py
Normal file
20
main.py
Normal file
|
|
@ -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()
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
tkcalendar==1.6.1
|
||||
nuitka>=2.0.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue