Compare commits
No commits in common. "main" and "dev-upd" have entirely different histories.
22 changed files with 1856 additions and 0 deletions
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
|
||||
# Виртуальное окружение
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# База данных и данные приложения
|
||||
captains_log.db
|
||||
*.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Nuitka
|
||||
*.build/
|
||||
*.dist/
|
||||
*.onefile-build/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
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 - Приложение для быстрых заметок
|
||||
"""
|
||||
55
app/auth.py
Normal file
55
app/auth.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Модуль аутентификации"""
|
||||
import hashlib
|
||||
import os
|
||||
from app.database import Database
|
||||
|
||||
class Auth:
|
||||
def __init__(self, db: Database):
|
||||
self.db = db
|
||||
|
||||
def set_master_password(self, password):
|
||||
"""Установить мастер-пароль"""
|
||||
password_hash = self._hash_password(password)
|
||||
self.db.set_setting('master_password_hash', password_hash)
|
||||
self.db.set_setting('password_set', '1')
|
||||
|
||||
def check_password(self, password):
|
||||
"""Проверить мастер-пароль"""
|
||||
stored_hash = self.db.get_setting('master_password_hash')
|
||||
if not stored_hash:
|
||||
return True # Пароль не установлен
|
||||
|
||||
return self._hash_password(password) == stored_hash
|
||||
|
||||
def is_password_set(self):
|
||||
"""Проверяет, установлен ли пароль"""
|
||||
return self.db.get_setting('password_set', '0') == '1'
|
||||
|
||||
def ask_password_on_startup(self):
|
||||
"""Спрашивать ли пароль при входе"""
|
||||
return self.db.get_setting('ask_password_on_startup', '1') == '1'
|
||||
|
||||
def set_ask_password_on_startup(self, value):
|
||||
"""Установить, спрашивать ли пароль при входе"""
|
||||
self.db.set_setting('ask_password_on_startup', '1' if value else '0')
|
||||
|
||||
def reset_password(self):
|
||||
"""Сбросить мастер-пароль"""
|
||||
self.db.set_setting('password_set', '0')
|
||||
self.db.set_setting('master_password_hash', '')
|
||||
self.db.set_setting('ask_password_on_startup', '1')
|
||||
|
||||
def change_password(self, old_password, new_password):
|
||||
"""Изменить мастер-пароль"""
|
||||
if self.is_password_set():
|
||||
if not self.check_password(old_password):
|
||||
return False, "Неверный текущий пароль"
|
||||
|
||||
self.set_master_password(new_password)
|
||||
return True, "Пароль успешно изменен"
|
||||
|
||||
def _hash_password(self, password):
|
||||
"""Хешировать пароль с солью"""
|
||||
# В будущем здесь будет более безопасное хеширование
|
||||
# Пока используем SHA-256 для простоты
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
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 модули"""
|
||||
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)
|
||||
296
app/ui/dialogs.py
Normal file
296
app/ui/dialogs.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"""Диалоговые окна"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from app.config import Config
|
||||
|
||||
class PasswordDialog(tk.Toplevel):
|
||||
"""Диалог установки/ввода мастер-пароля"""
|
||||
|
||||
def __init__(self, parent, auth, mode='set'):
|
||||
super().__init__(parent)
|
||||
self.auth = auth
|
||||
self.mode = mode # 'set', 'check', 'reset'
|
||||
self.result = False
|
||||
|
||||
self.title("Мастер-пароль" if mode == 'set' else
|
||||
"Введите пароль" if mode == 'check' else
|
||||
"Сброс пароля")
|
||||
self.geometry("400x300" if mode == 'reset' else "400x250")
|
||||
self.resizable(False, False)
|
||||
self.configure(bg=Config.COLORS['bg'])
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
# Центрируем окно
|
||||
self.update_idletasks()
|
||||
x = parent.winfo_rootx() + (parent.winfo_width() - 400) // 2
|
||||
y = parent.winfo_rooty() + (parent.winfo_height() - 250) // 2
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
def create_widgets(self):
|
||||
main_frame = tk.Frame(self, bg=Config.COLORS['bg'])
|
||||
main_frame.pack(expand=True, fill='both', padx=20, pady=20)
|
||||
|
||||
if self.mode == 'set':
|
||||
text = "Придумайте мастер-пароль\nдля защиты ваших заметок:"
|
||||
elif self.mode == 'check':
|
||||
text = "Введите мастер-пароль:"
|
||||
else: # reset
|
||||
text = "ВНИМАНИЕ!\nЭто сбросит текущий пароль.\nВсе заметки останутся.\n\nДля сброса введите 'RESET':"
|
||||
|
||||
tk.Label(
|
||||
main_frame,
|
||||
text=text,
|
||||
bg=Config.COLORS['bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 12),
|
||||
wraplength=350
|
||||
).pack(pady=(0, 15))
|
||||
|
||||
if self.mode == 'reset':
|
||||
# Поле для сброса
|
||||
self.reset_var = tk.StringVar()
|
||||
self.reset_entry = ttk.Entry(
|
||||
main_frame,
|
||||
textvariable=self.reset_var,
|
||||
font=('Arial', 12)
|
||||
)
|
||||
self.reset_entry.pack(fill='x', pady=(0, 15))
|
||||
self.reset_entry.focus()
|
||||
|
||||
tk.Button(
|
||||
main_frame,
|
||||
text="Сбросить пароль",
|
||||
command=self.reset_password,
|
||||
bg=Config.COLORS['danger'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 11, 'bold'),
|
||||
relief='flat',
|
||||
cursor='hand2'
|
||||
).pack(fill='x', pady=(0, 5))
|
||||
|
||||
tk.Button(
|
||||
main_frame,
|
||||
text="Отмена",
|
||||
command=self.destroy,
|
||||
bg=Config.COLORS['button_bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
relief='flat',
|
||||
cursor='hand2'
|
||||
).pack(fill='x')
|
||||
else:
|
||||
# Поле ввода пароля
|
||||
self.password_var = tk.StringVar()
|
||||
self.password_entry = ttk.Entry(
|
||||
main_frame,
|
||||
textvariable=self.password_var,
|
||||
show='*',
|
||||
font=('Arial', 12)
|
||||
)
|
||||
self.password_entry.pack(fill='x', pady=(0, 10))
|
||||
self.password_entry.focus()
|
||||
|
||||
if self.mode == 'set':
|
||||
# Подтверждение пароля
|
||||
tk.Label(
|
||||
main_frame,
|
||||
text="Подтвердите пароль:",
|
||||
bg=Config.COLORS['bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 11)
|
||||
).pack()
|
||||
|
||||
self.confirm_var = tk.StringVar()
|
||||
self.confirm_entry = ttk.Entry(
|
||||
main_frame,
|
||||
textvariable=self.confirm_var,
|
||||
show='*',
|
||||
font=('Arial', 12)
|
||||
)
|
||||
self.confirm_entry.pack(fill='x', pady=(0, 15))
|
||||
|
||||
# Чекбокс "спрашивать при входе"
|
||||
self.ask_var = tk.BooleanVar(value=True)
|
||||
tk.Checkbutton(
|
||||
main_frame,
|
||||
text="Спрашивать при входе",
|
||||
variable=self.ask_var,
|
||||
bg=Config.COLORS['bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
selectcolor=Config.COLORS['bg'],
|
||||
activebackground=Config.COLORS['bg'],
|
||||
activeforeground=Config.COLORS['fg']
|
||||
).pack(pady=(0, 15))
|
||||
|
||||
# Кнопки
|
||||
button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg'])
|
||||
button_frame.pack(fill='x')
|
||||
|
||||
if self.mode == 'set':
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Установить пароль",
|
||||
command=self.set_password,
|
||||
bg=Config.COLORS['accent'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 11, 'bold'),
|
||||
relief='flat',
|
||||
cursor='hand2'
|
||||
).pack(fill='x', pady=(0, 5))
|
||||
else:
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Войти",
|
||||
command=self.check_password,
|
||||
bg=Config.COLORS['accent'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 11, 'bold'),
|
||||
relief='flat',
|
||||
cursor='hand2'
|
||||
).pack(fill='x', pady=(0, 5))
|
||||
|
||||
# Кнопка сброса пароля
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Забыли пароль?",
|
||||
command=self.show_reset_warning,
|
||||
bg=Config.COLORS['button_bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
relief='flat',
|
||||
cursor='hand2',
|
||||
font=('Arial', 9)
|
||||
).pack(fill='x', pady=(0, 5))
|
||||
|
||||
if self.mode == 'check':
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Отмена",
|
||||
command=self.destroy,
|
||||
bg=Config.COLORS['button_bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
relief='flat',
|
||||
cursor='hand2'
|
||||
).pack(fill='x')
|
||||
|
||||
def set_password(self):
|
||||
password = self.password_var.get()
|
||||
confirm = self.confirm_var.get()
|
||||
|
||||
if not password:
|
||||
messagebox.showwarning("Ошибка", "Введите пароль")
|
||||
return
|
||||
|
||||
if len(password) < 4:
|
||||
messagebox.showwarning("Ошибка", "Пароль должен быть минимум 4 символа")
|
||||
return
|
||||
|
||||
if password != confirm:
|
||||
messagebox.showwarning("Ошибка", "Пароли не совпадают")
|
||||
return
|
||||
|
||||
self.auth.set_master_password(password)
|
||||
self.auth.set_ask_password_on_startup(self.ask_var.get())
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
def check_password(self):
|
||||
password = self.password_var.get()
|
||||
|
||||
if self.auth.check_password(password):
|
||||
self.result = True
|
||||
self.destroy()
|
||||
else:
|
||||
messagebox.showerror("Ошибка", "Неверный пароль")
|
||||
self.password_var.set('')
|
||||
self.password_entry.focus()
|
||||
|
||||
def reset_password(self):
|
||||
if self.reset_var.get().upper() == 'RESET':
|
||||
self.auth.reset_password()
|
||||
messagebox.showinfo("Успех", "Пароль сброшен.\nПри следующем запуске создайте новый пароль.")
|
||||
self.result = True
|
||||
self.destroy()
|
||||
else:
|
||||
messagebox.showerror("Ошибка", "Введите RESET для подтверждения сброса")
|
||||
self.reset_var.set('')
|
||||
self.reset_entry.focus()
|
||||
|
||||
def show_reset_warning(self):
|
||||
"""Показать предупреждение о сбросе пароля"""
|
||||
if messagebox.askyesno(
|
||||
"Сброс пароля",
|
||||
"Вы уверены, что хотите сбросить пароль?\n\n"
|
||||
"Это действие удалит только пароль, все заметки останутся.\n"
|
||||
"После сброса вы сможете установить новый пароль."
|
||||
):
|
||||
self.destroy()
|
||||
reset_dialog = PasswordDialog(self.master, self.auth, mode='reset')
|
||||
self.master.wait_window(reset_dialog)
|
||||
if reset_dialog.result:
|
||||
self.result = True
|
||||
self.destroy()
|
||||
|
||||
|
||||
class ConfirmDialog(tk.Toplevel):
|
||||
"""Диалог подтверждения действия"""
|
||||
|
||||
def __init__(self, parent, title, message):
|
||||
super().__init__(parent)
|
||||
self.result = False
|
||||
|
||||
self.title(title)
|
||||
self.geometry("350x150")
|
||||
self.resizable(False, False)
|
||||
self.configure(bg=Config.COLORS['bg'])
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
# Центрируем окно
|
||||
self.update_idletasks()
|
||||
x = parent.winfo_rootx() + (parent.winfo_width() - 350) // 2
|
||||
y = parent.winfo_rooty() + (parent.winfo_height() - 150) // 2
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
# Сообщение
|
||||
tk.Label(
|
||||
self,
|
||||
text=message,
|
||||
bg=Config.COLORS['bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
font=('Arial', 11),
|
||||
wraplength=320
|
||||
).pack(expand=True, pady=(20, 10))
|
||||
|
||||
# Кнопки
|
||||
btn_frame = tk.Frame(self, bg=Config.COLORS['bg'])
|
||||
btn_frame.pack(fill='x', padx=20, pady=(0, 20))
|
||||
|
||||
tk.Button(
|
||||
btn_frame,
|
||||
text="Да",
|
||||
command=self.confirm,
|
||||
bg=Config.COLORS['danger'],
|
||||
fg=Config.COLORS['fg'],
|
||||
relief='flat',
|
||||
cursor='hand2',
|
||||
width=10
|
||||
).pack(side='left', padx=(0, 10))
|
||||
|
||||
tk.Button(
|
||||
btn_frame,
|
||||
text="Нет",
|
||||
command=self.destroy,
|
||||
bg=Config.COLORS['button_bg'],
|
||||
fg=Config.COLORS['fg'],
|
||||
relief='flat',
|
||||
cursor='hand2',
|
||||
width=10
|
||||
).pack(side='left')
|
||||
|
||||
def confirm(self):
|
||||
self.result = True
|
||||
self.destroy()
|
||||
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
|
||||
61
icon.svg
Normal file
61
icon.svg
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<path style="fill:#ECE5DD;" d="M45.208,61.44h421.583c20.768,0,37.664,16.896,37.664,37.663v367.688
|
||||
c0,20.768-16.896,37.664-37.663,37.664H45.208c-20.767,0-37.663-16.896-37.663-37.663v-59.348V99.103
|
||||
C7.545,78.336,24.441,61.44,45.208,61.44z"/>
|
||||
<path style="fill:#D9CBBD;" d="M470.995,99.103v315.148h23.655l9.805-8.553V99.103c0-20.767-16.896-37.663-37.663-37.663h-33.46
|
||||
C454.099,61.44,470.995,78.336,470.995,99.103z"/>
|
||||
<g>
|
||||
<path style="fill:#B39A7C;" d="M504.455,99.103c0-20.767-16.896-37.663-37.663-37.663H45.208
|
||||
c-20.767,0-37.663,16.896-37.663,37.663v70.666v9.243h496.909V99.103z"/>
|
||||
<path style="fill:#B39A7C;" d="M7.545,99.103c0-20.767,16.896-37.663,37.663-37.663h421.583c20.768,0,37.664,16.896,37.664,37.663
|
||||
v70.666v9.243H7.545V99.103z"/>
|
||||
</g>
|
||||
<path style="fill:#957755;" d="M466.792,61.44H433.69c20.768,0,37.663,16.896,37.663,37.663v79.908h33.102V99.103
|
||||
C504.455,78.336,487.559,61.44,466.792,61.44z"/>
|
||||
<g>
|
||||
<rect x="8.084" y="226.358" style="fill:#BABABA;" width="449.687" height="16.168"/>
|
||||
<rect x="8.084" y="291.032" style="fill:#BABABA;" width="449.687" height="16.168"/>
|
||||
<rect x="8.084" y="355.705" style="fill:#BABABA;" width="449.687" height="16.168"/>
|
||||
<rect x="8.084" y="420.4" style="fill:#BABABA;" width="388.042" height="16.168"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFB656;" d="M354.186,115.549c-25.977-4.647-44.831-27.177-44.831-53.57c0-30.015,24.419-54.434,54.434-54.434
|
||||
c26.393,0,48.923,18.853,53.57,44.831l1.815,10.142h-33.39l-1.779-6.267c-2.557-9.004-10.871-15.291-20.217-15.291
|
||||
c-11.59,0-21.019,9.429-21.019,21.019c0,9.354,6.287,17.672,15.29,20.227l6.268,1.779v33.38L354.186,115.549z"/>
|
||||
<path style="fill:#FFB656;" d="M246.397,115.549c-25.977-4.647-44.831-27.177-44.831-53.57c0-30.015,24.419-54.434,54.434-54.434
|
||||
c26.393,0,48.923,18.853,53.57,44.831l1.815,10.142h-33.39l-1.779-6.267C273.66,47.247,265.346,40.96,256,40.96
|
||||
c-11.59,0-21.019,9.429-21.019,21.019c0,9.354,6.287,17.672,15.29,20.227l6.268,1.779v33.38L246.397,115.549z"/>
|
||||
<path style="fill:#FFB656;" d="M138.608,115.549c-25.977-4.647-44.831-27.177-44.831-53.57c0-30.015,24.419-54.434,54.434-54.434
|
||||
c26.393,0,48.923,18.853,53.57,44.831l1.815,10.142h-33.39l-1.779-6.267c-2.557-9.004-10.871-15.291-20.217-15.291
|
||||
c-11.59,0-21.019,9.429-21.019,21.019c0,9.354,6.287,17.672,15.29,20.227l6.268,1.779v33.38L138.608,115.549z"/>
|
||||
</g>
|
||||
<path style="fill:#C0AB91;" d="M395.876,428.501c0-12.205,9.93-22.135,22.135-22.135l86.444-0.668l-109.442,98.757L395.876,428.501z
|
||||
"/>
|
||||
<rect x="102.4" y="99.845" style="fill:#FF6342;" width="16.168" height="404.609"/>
|
||||
<path d="M466.792,53.895h-41.568C421.248,23.527,395.224,0,363.789,0c-23.072,0-43.226,12.681-53.895,31.43
|
||||
C299.226,12.681,279.072,0,256,0s-43.226,12.681-53.895,31.43C191.436,12.681,171.283,0,148.211,0
|
||||
c-31.435,0-57.458,23.527-61.435,53.895H45.208C20.28,53.895,0,74.174,0,99.103v367.688C0,491.72,20.28,512,45.208,512h421.583
|
||||
C491.72,512,512,491.72,512,466.792V99.103C512,74.174,491.72,53.895,466.792,53.895z M363.789,16.168
|
||||
c22.501,0,41.251,16.311,45.082,37.726h-16.569c-3.529-12.426-14.971-21.558-28.512-21.558c-16.344,0-29.642,13.298-29.642,29.642
|
||||
c0,13.543,9.13,24.994,21.558,28.522v16.56c-21.416-3.831-37.726-22.581-37.726-45.082
|
||||
C317.979,36.718,338.529,16.168,363.789,16.168z M374.543,53.895h-21.507c2.461-3.265,6.358-5.389,10.753-5.389
|
||||
C368.183,48.505,372.082,50.63,374.543,53.895z M256,16.168c22.501,0,41.251,16.311,45.082,37.726h-16.569
|
||||
C280.983,41.469,269.542,32.337,256,32.337c-16.344,0-29.642,13.298-29.642,29.642c0,13.543,9.13,24.994,21.558,28.522v16.56
|
||||
c-21.416-3.831-37.726-22.581-37.726-45.082C210.189,36.718,230.74,16.168,256,16.168z M266.753,53.895h-21.507
|
||||
c2.461-3.265,6.358-5.389,10.753-5.389C260.393,48.505,264.292,50.63,266.753,53.895z M148.211,16.168
|
||||
c22.501,0,41.251,16.311,45.082,37.726h-16.569c-3.529-12.426-14.971-21.558-28.512-21.558c-16.344,0-29.642,13.298-29.642,29.642
|
||||
c0,13.543,9.13,24.994,21.558,28.522v16.56C118.711,103.23,102.4,84.48,102.4,61.979C102.4,36.718,122.95,16.168,148.211,16.168z
|
||||
M158.964,53.895h-21.507c2.461-3.265,6.359-5.389,10.753-5.389C152.604,48.505,156.503,50.63,158.964,53.895z M495.832,466.792
|
||||
c0,16.012-13.027,29.04-29.04,29.04h-49.597l78.637-70.773V466.792z M495.832,398.821h-26.243c-4.465,0-8.084,3.62-8.084,8.084l0,0
|
||||
c0,4.465,3.62,8.084,8.084,8.084h13.26l-78.638,70.775v-57.263c0-7.462,6.049-13.511,13.511-13.511h16.221
|
||||
c4.465,0,8.084-3.62,8.084-8.084l0,0c0-4.465-3.62-8.084-8.084-8.084h-16.221c-16.392,0-29.68,13.288-29.68,29.68v67.331H45.208
|
||||
c-16.038,0-29.04-13.002-29.04-29.04V99.103c0-16.038,13.002-29.04,29.04-29.04h41.568c3.629,27.719,25.631,49.721,53.35,53.35
|
||||
v2.901c0,4.427,3.412,8.287,7.836,8.419c4.578,0.137,8.332-3.533,8.332-8.081V83.427c0-4.484-3.694-7.944-8.178-7.974
|
||||
c-4.356-0.03-8.216-2.147-10.659-5.389h57.108c3.629,27.719,25.631,49.721,53.35,53.35v2.901c0,4.427,3.412,8.287,7.836,8.419
|
||||
c4.578,0.137,8.332-3.533,8.332-8.081V83.427c0-4.484-3.694-7.944-8.178-7.974c-4.356-0.03-8.216-2.147-10.659-5.389h57.108
|
||||
c3.629,27.719,25.631,49.721,53.35,53.35v2.901c0,4.427,3.412,8.287,7.836,8.419c4.578,0.137,8.332-3.533,8.332-8.081V83.427
|
||||
c0-4.484-3.694-7.944-8.178-7.974c-4.356-0.03-8.216-2.147-10.659-5.389h113.756c16.038,0,29.04,13.002,29.04,29.04V398.821z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
73
main.py
Normal file
73
main.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Captain's Log - Приложение для быстрых заметок
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Добавляем текущую директорию в путь для импорта
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.ui.main_window import MainWindow
|
||||
|
||||
|
||||
def create_gitignore():
|
||||
"""Создать .gitignore если его нет"""
|
||||
gitignore_path = os.path.join(os.path.dirname(__file__), '.gitignore')
|
||||
|
||||
if not os.path.exists(gitignore_path):
|
||||
gitignore_content = """# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
|
||||
# Виртуальное окружение
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# База данных и данные приложения
|
||||
captains_log.db
|
||||
*.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Nuitka
|
||||
*.build/
|
||||
*.dist/
|
||||
*.onefile-build/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
"""
|
||||
try:
|
||||
with open(gitignore_path, 'w') as f:
|
||||
f.write(gitignore_content)
|
||||
except:
|
||||
pass # Игнорируем ошибки при создании .gitignore
|
||||
|
||||
|
||||
def main():
|
||||
"""Точка входа в приложение"""
|
||||
# Создаем .gitignore при первом запуске
|
||||
create_gitignore()
|
||||
|
||||
app = MainWindow()
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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