Compare commits

..

No commits in common. "9b0a20aa913d9fd6d578da9bb62783c4d9650c4d" and "main" have entirely different histories.

44 changed files with 0 additions and 1795 deletions

35
.gitignore vendored
View file

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

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

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

View file

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

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

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

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -1,3 +0,0 @@
"""
Captain's Log - Приложение для быстрых заметок
"""

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

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

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

73
main.py
View file

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

View file

@ -1,2 +0,0 @@
tkcalendar==1.6.1
nuitka>=2.0.0