Compare commits
No commits in common. "9b0a20aa913d9fd6d578da9bb62783c4d9650c4d" and "main" have entirely different histories.
9b0a20aa91
...
main
44 changed files with 0 additions and 1795 deletions
35
.gitignore
vendored
35
.gitignore
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
# 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
|
|
||||||
10
.idea/.gitignore
generated
vendored
10
.idea/.gitignore
generated
vendored
|
|
@ -1,10 +0,0 @@
|
||||||
# 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
10
.idea/CapLog.iml
generated
|
|
@ -1,10 +0,0 @@
|
||||||
<?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
6
.idea/inspectionProfiles/profiles_settings.xml
generated
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
|
|
@ -1,7 +0,0 @@
|
||||||
<?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
8
.idea/modules.xml
generated
|
|
@ -1,8 +0,0 @@
|
||||||
<?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
6
.idea/vcs.xml
generated
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
"""
|
|
||||||
Captain's Log - Приложение для быстрых заметок
|
|
||||||
"""
|
|
||||||
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.
55
app/auth.py
55
app/auth.py
|
|
@ -1,55 +0,0 @@
|
||||||
"""Модуль аутентификации"""
|
|
||||||
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()
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
"""Конфигурация приложения"""
|
|
||||||
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
245
app/database.py
|
|
@ -1,245 +0,0 @@
|
||||||
"""Модуль для работы с базой данных 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()]
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
"""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.
|
|
@ -1,189 +0,0 @@
|
||||||
"""Виджет календаря"""
|
|
||||||
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)
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
"""Диалоговые окна"""
|
|
||||||
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()
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
"""Главное окно приложения"""
|
|
||||||
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()
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
"""Редактор заметок"""
|
|
||||||
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)
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
"""Список заметок"""
|
|
||||||
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)
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
"""Боковая панель"""
|
|
||||||
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
BIN
captains_log.db
Binary file not shown.
73
main.py
73
main.py
|
|
@ -1,73 +0,0 @@
|
||||||
#!/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()
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
tkcalendar==1.6.1
|
|
||||||
nuitka>=2.0.0
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue