"""Наложение одной даты на одинаковые строки отсканированного PDF."""

from __future__ import annotations

import base64
import json
import os
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk

try:
    # Современное имя пакета (PyMuPDF 1.24+).
    import pymupdf as fitz
except ImportError:
    # Совместимость с более старыми выпусками PyMuPDF.
    import fitz


APP_TITLE = "Дата на сканированном PDF"
TIMES_NEW_ROMAN_FILE = Path(
    os.environ.get("WINDIR", r"C:\Windows")
) / "Fonts" / "times.ttf"
LAST_SETTINGS_FILE = (
    Path(os.environ.get("APPDATA", Path.home()))
    / "PdfDateApp"
    / "last_settings.json"
)

# Значения легко изменить здесь; те же параметры доступны в интерфейсе.
DEFAULT_SETTINGS = {
    "x": 40.0,
    "start_y": 120.0,
    "row_step": 28.0,
    "rows_per_page": 20,
    "font_size": 10.0,
}


class PdfDateApp(tk.Tk):
    """Главное окно программы."""

    def __init__(self) -> None:
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("760x480")
        self.minsize(680, 450)

        self.pdf_path = tk.StringVar()
        self.date_text = tk.StringVar()
        self.setting_vars = {
            key: tk.StringVar(value=str(value))
            for key, value in DEFAULT_SETTINGS.items()
        }
        self.current_page = tk.IntVar(value=1)
        self.page_counter_text = tk.StringVar(value="из 1")
        self.page_count = 1
        self.settings_pdf_path: str | None = None
        self.page_settings: dict[int, dict[str, str]] = {
            1: {key: var.get() for key, var in self.setting_vars.items()}
        }
        self.loading_page_settings = False

        self._load_last_settings()

        # Ссылки на окно живого предпросмотра и отложенное обновление.
        self.preview_window: tk.Toplevel | None = None
        self.preview_canvas: tk.Canvas | None = None
        self.preview_status: ttk.Label | None = None
        self.preview_image: tk.PhotoImage | None = None
        self.preview_after_id: str | None = None

        self._build_ui()
        self._install_live_preview_traces()
        self.protocol("WM_DELETE_WINDOW", self._on_close)

    def _build_ui(self) -> None:
        """Создаёт элементы интерфейса."""
        main = ttk.Frame(self, padding=16)
        main.pack(fill="both", expand=True)
        main.columnconfigure(1, weight=1)

        ttk.Label(main, text="PDF-файл:").grid(
            row=0, column=0, sticky="w", padx=(0, 8), pady=6
        )
        ttk.Entry(main, textvariable=self.pdf_path).grid(
            row=0, column=1, sticky="ew", pady=6
        )
        ttk.Button(main, text="Выбрать PDF...", command=self.choose_pdf).grid(
            row=0, column=2, padx=(8, 0), pady=6
        )

        ttk.Label(main, text="Дата (например, 20.06):").grid(
            row=1, column=0, sticky="w", padx=(0, 8), pady=6
        )
        date_entry = ttk.Entry(main, textvariable=self.date_text, width=20)
        date_entry.grid(row=1, column=1, sticky="w", pady=6)

        settings_box = ttk.LabelFrame(
            main, text="Положение даты для выбранной страницы (в пунктах PDF)", padding=12
        )
        settings_box.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(12, 8))
        for column in range(4):
            settings_box.columnconfigure(column, weight=1 if column in (1, 3) else 0)

        ttk.Label(settings_box, text="Страница:").grid(
            row=0, column=0, sticky="w", padx=(0, 8), pady=(0, 9)
        )
        self.page_spinbox = ttk.Spinbox(
            settings_box,
            from_=1,
            to=1,
            textvariable=self.current_page,
            width=8,
            state="readonly",
        )
        self.page_spinbox.grid(row=0, column=1, sticky="w", pady=(0, 9))
        ttk.Label(settings_box, textvariable=self.page_counter_text).grid(
            row=0, column=1, sticky="w", padx=(70, 0), pady=(0, 9)
        )

        fields = [
            ("X-координата:", "x"),
            ("Начальная Y-координата:", "start_y"),
            ("Шаг между строками:", "row_step"),
            ("Количество строк:", "rows_per_page"),
            ("Размер шрифта:", "font_size"),
        ]
        for index, (label, key) in enumerate(fields):
            row, pair = divmod(index, 2)
            row += 1
            label_column = pair * 2
            ttk.Label(settings_box, text=label).grid(
                row=row, column=label_column, sticky="w", padx=(0, 8), pady=5
            )
            ttk.Entry(
                settings_box, textvariable=self.setting_vars[key], width=14
            ).grid(row=row, column=label_column + 1, sticky="w", padx=(0, 18), pady=5)

        ttk.Label(
            main,
            text=(
                "X и Y отсчитываются от верхнего левого угла страницы. "
                "Y задаёт базовую линию первой даты."
            ),
            foreground="#555555",
        ).grid(row=3, column=0, columnspan=3, sticky="w", pady=(0, 12))

        settings_buttons = ttk.Frame(main)
        settings_buttons.grid(row=4, column=0, columnspan=3, sticky="w")
        ttk.Button(
            settings_buttons, text="Сохранить настройки...", command=self.save_settings
        ).pack(side="left", padx=(0, 8))
        ttk.Button(
            settings_buttons, text="Загрузить настройки...", command=self.load_settings
        ).pack(side="left")

        action_buttons = ttk.Frame(main)
        action_buttons.grid(row=5, column=0, columnspan=3, sticky="ew", pady=(24, 0))
        action_buttons.columnconfigure(0, weight=1)
        ttk.Button(
            action_buttons,
            text="Предпросмотр выбранной страницы",
            command=self.preview_first_page,
        ).grid(row=0, column=1, padx=(0, 10))
        ttk.Button(
            action_buttons, text="Сохранить PDF с датой", command=self.create_pdf
        ).grid(row=0, column=2)

        date_entry.focus_set()

    def choose_pdf(self) -> None:
        path = filedialog.askopenfilename(
            title="Выберите PDF-файл",
            filetypes=[("PDF-файлы", "*.pdf"), ("Все файлы", "*.*")],
        )
        if path:
            self.pdf_path.set(path)
            try:
                self._prepare_page_settings(Path(path))
            except Exception as exc:
                messagebox.showerror(APP_TITLE, f"Не удалось открыть PDF:\n{exc}")

    @staticmethod
    def _to_float(value: str, field_name: str) -> float:
        """Принимает как точку, так и запятую в дробном числе."""
        try:
            return float(value.strip().replace(",", "."))
        except ValueError as exc:
            raise ValueError(f"Поле «{field_name}» должно содержать число.") from exc

    def _parse_settings_values(
        self, values: dict[str, str]
    ) -> dict[str, float | int]:
        """Проверяет один набор настроек страницы."""
        x = self._to_float(values["x"], "X-координата")
        start_y = self._to_float(
            values["start_y"], "Начальная Y-координата"
        )
        row_step = self._to_float(
            values["row_step"], "Шаг между строками"
        )
        font_size = self._to_float(
            values["font_size"], "Размер шрифта"
        )
        try:
            rows = int(values["rows_per_page"].strip())
        except ValueError as exc:
            raise ValueError("Поле «Количество строк» должно содержать целое число.") from exc

        if x < 0 or start_y < 0:
            raise ValueError("Координаты X и Y не могут быть отрицательными.")
        if row_step <= 0:
            raise ValueError("Шаг между строками должен быть больше нуля.")
        if rows <= 0:
            raise ValueError("Количество строк должно быть больше нуля.")
        if font_size <= 0:
            raise ValueError("Размер шрифта должен быть больше нуля.")

        return {
            "x": x,
            "start_y": start_y,
            "row_step": row_step,
            "rows_per_page": rows,
            "font_size": font_size,
        }

    def get_settings(self) -> dict[str, float | int]:
        """Читает и проверяет настройки выбранной страницы из формы."""
        return self._parse_settings_values(
            {key: variable.get() for key, variable in self.setting_vars.items()}
        )

    def _current_raw_settings(self) -> dict[str, str]:
        return {key: variable.get() for key, variable in self.setting_vars.items()}

    def _load_last_settings(self) -> None:
        """Restores the most recently used placement settings."""
        try:
            with LAST_SETTINGS_FILE.open("r", encoding="utf-8") as file:
                loaded = json.load(file)
            if not isinstance(loaded, dict):
                return
            raw_values = {key: str(loaded[key]) for key in DEFAULT_SETTINGS}
            self._parse_settings_values(raw_values)
        except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
            return

        for key, value in raw_values.items():
            self.setting_vars[key].set(value)
        self.page_settings[1] = raw_values.copy()

    def _save_last_settings(self) -> None:
        """Stores the current placement settings for the next launch."""
        raw_values = self._current_raw_settings()
        try:
            self._parse_settings_values(raw_values)
            LAST_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
            with LAST_SETTINGS_FILE.open("w", encoding="utf-8") as file:
                json.dump(raw_values, file, ensure_ascii=False, indent=2)
        except (OSError, ValueError):
            # Closing the application must not be blocked by an invalid field or
            # by an unavailable user settings directory.
            return

    def _on_close(self) -> None:
        self._save_last_settings()
        self.destroy()

    def _prepare_page_settings(self, path: Path) -> None:
        """Создаёт отдельный профиль для каждой страницы нового PDF."""
        resolved_path = str(path.resolve())
        if self.settings_pdf_path == resolved_path:
            return

        document = self.open_pdf(path)
        try:
            page_count = document.page_count
        finally:
            document.close()

        template = self._current_raw_settings()
        self.page_count = page_count
        self.settings_pdf_path = resolved_path
        self.page_settings = {
            page_number: template.copy()
            for page_number in range(1, page_count + 1)
        }
        self.page_spinbox.configure(to=page_count)
        self.page_counter_text.set(f"из {page_count}")
        self.loading_page_settings = True
        try:
            self.current_page.set(1)
            self._load_page_settings_into_form(1)
        finally:
            self.loading_page_settings = False

    def _load_page_settings_into_form(self, page_number: int) -> None:
        values = self.page_settings.get(page_number)
        if values is None:
            values = {
                key: str(value) for key, value in DEFAULT_SETTINGS.items()
            }
            self.page_settings[page_number] = values
        for key, value in values.items():
            self.setting_vars[key].set(value)

    def _on_setting_changed(self, *_args: object) -> None:
        """Сразу запоминает введённые значения для выбранной страницы."""
        if not self.loading_page_settings:
            try:
                page_number = int(self.current_page.get())
            except (tk.TclError, ValueError):
                return
            self.page_settings[page_number] = self._current_raw_settings()
        self._schedule_live_preview()

    def _on_page_changed(self, *_args: object) -> None:
        """Подставляет настройки выбранной страницы в поля формы."""
        if self.loading_page_settings:
            return
        try:
            page_number = int(self.current_page.get())
        except (tk.TclError, ValueError):
            return
        if not 1 <= page_number <= self.page_count:
            return

        self.loading_page_settings = True
        try:
            self._load_page_settings_into_form(page_number)
        finally:
            self.loading_page_settings = False
        self._schedule_live_preview()

    def validate_input(self) -> tuple[Path, str, dict[str, float | int]] | None:
        """Проверяет файл, дату и числовые поля."""
        raw_path = self.pdf_path.get().strip()
        if not raw_path:
            messagebox.showerror(APP_TITLE, "PDF-файл не выбран.")
            return None

        path = Path(raw_path)
        if not path.is_file():
            messagebox.showerror(APP_TITLE, "Выбранный PDF-файл не найден.")
            return None

        try:
            self._prepare_page_settings(path)
        except Exception as exc:
            messagebox.showerror(APP_TITLE, f"PDF не открывается:\n{exc}")
            return None

        date = self.date_text.get().strip()
        if not date:
            messagebox.showerror(APP_TITLE, "Дата не введена.")
            return None

        try:
            settings = self.get_settings()
        except ValueError as exc:
            messagebox.showerror(APP_TITLE, str(exc))
            return None

        return path, date, settings

    @staticmethod
    def add_dates(page: fitz.Page, date: str, settings: dict[str, float | int]) -> None:
        """Накладывает дату на каждую заданную строку страницы."""
        x = float(settings["x"])
        start_y = float(settings["start_y"])
        step = float(settings["row_step"])
        rows = int(settings["rows_per_page"])
        font_size = float(settings["font_size"])

        # На Windows используем настоящий Times New Roman и встраиваем его в PDF.
        # Встроенный Times Roman служит запасным вариантом для других систем.
        if TIMES_NEW_ROMAN_FILE.is_file():
            font_options = {
                "fontname": "TimesNewRoman",
                "fontfile": str(TIMES_NEW_ROMAN_FILE),
            }
        else:
            font_options = {"fontname": "tiro"}

        for row_number in range(rows):
            y = start_y + row_number * step
            page.insert_text(
                fitz.Point(x, y),
                date,
                fontsize=font_size,
                color=(0, 0, 0),
                overlay=True,
                **font_options,
            )

    @staticmethod
    def open_pdf(path: Path) -> fitz.Document:
        """Открывает PDF и выдаёт понятную ошибку для пустого документа."""
        document = fitz.open(path)
        if document.page_count == 0:
            document.close()
            raise ValueError("PDF не содержит страниц.")
        return document

    @staticmethod
    def date_for_filename(date: str) -> str:
        """Returns the entered date in a form safe for a Windows file name."""
        invalid_characters = '<>:"/\\|?*'
        return "".join(
            "." if character in invalid_characters else character
            for character in date.strip()
        ).strip(" .")

    def preview_first_page(self) -> None:
        """Открывает окно, которое обновляется при каждом изменении параметров."""
        validated = self.validate_input()
        if validated is None:
            return
        self.show_preview_window()
        self._refresh_live_preview()

    def _install_live_preview_traces(self) -> None:
        """Следит за всеми полями, влияющими на изображение."""
        for variable in (self.pdf_path, self.date_text):
            variable.trace_add("write", self._schedule_live_preview)
        for variable in self.setting_vars.values():
            variable.trace_add("write", self._on_setting_changed)
        self.current_page.trace_add("write", self._on_page_changed)

    def _schedule_live_preview(self, *_args: object) -> None:
        """Перерисовывает после короткой паузы, не блокируя ввод с клавиатуры."""
        if self.preview_window is None or not self.preview_window.winfo_exists():
            return
        if self.preview_after_id is not None:
            self.after_cancel(self.preview_after_id)
        self.preview_after_id = self.after(250, self._refresh_live_preview)

    def _set_preview_status(self, text: str, is_error: bool = False) -> None:
        if self.preview_status is not None:
            self.preview_status.configure(
                text=text,
                foreground="#a00000" if is_error else "#333333",
            )

    def _refresh_live_preview(self) -> None:
        """Формирует актуальную первую страницу полностью в оперативной памяти."""
        self.preview_after_id = None
        if self.preview_window is None or not self.preview_window.winfo_exists():
            return

        raw_path = self.pdf_path.get().strip()
        date = self.date_text.get().strip()
        if not raw_path or not Path(raw_path).is_file():
            self._set_preview_status("Выберите существующий PDF-файл.", True)
            return
        if not date:
            self._set_preview_status("Введите дату.", True)
            return
        try:
            settings = self.get_settings()
        except ValueError as exc:
            # Во время набора поле кратковременно может быть пустым — без pop-up.
            self._set_preview_status(str(exc), True)
            return

        self._set_preview_status("Обновление предпросмотра...")
        self.update_idletasks()
        source = None
        preview_doc = None
        try:
            source = self.open_pdf(Path(raw_path))
            page_number = int(self.current_page.get())
            if not 1 <= page_number <= source.page_count:
                raise ValueError("Выбрана несуществующая страница.")
            source_page = source[page_number - 1]

            # Копия страницы создаётся только в оперативной памяти.
            preview_doc = fitz.open()
            preview_page = preview_doc.new_page(
                width=source_page.rect.width, height=source_page.rect.height
            )
            preview_page.show_pdf_page(preview_page.rect, source, page_number - 1)
            self.add_dates(preview_page, date, settings)

            # Умеренный масштаб сохраняет читаемость и не расходует лишнюю память.
            pixmap = preview_page.get_pixmap(
                matrix=fitz.Matrix(1.35, 1.35), alpha=False
            )
            png_base64 = base64.b64encode(pixmap.tobytes("png"))
            image = tk.PhotoImage(data=png_base64)

            if self.preview_canvas is not None:
                self.preview_canvas.delete("all")
                self.preview_canvas.create_image(12, 12, image=image, anchor="nw")
                self.preview_canvas.configure(
                    scrollregion=(0, 0, pixmap.width + 24, pixmap.height + 24)
                )
            self.preview_image = image
            self._set_preview_status(
                f"Страница {page_number} из {source.page_count} — предпросмотр актуален."
            )
        except Exception as exc:
            self._set_preview_status(f"Ошибка предпросмотра: {exc}", True)
        finally:
            if preview_doc is not None:
                preview_doc.close()
            if source is not None:
                source.close()

    def show_preview_window(self) -> None:
        """Показывает прокручиваемое окно живого предпросмотра."""
        if self.preview_window is not None and self.preview_window.winfo_exists():
            self.preview_window.deiconify()
            self.preview_window.lift()
            return

        window = tk.Toplevel(self)
        self.preview_window = window
        window.title("Живой предпросмотр выбранной страницы")
        window.geometry("900x760")
        window.minsize(500, 400)
        window.protocol("WM_DELETE_WINDOW", self._close_preview_window)

        frame = ttk.Frame(window)
        frame.pack(fill="both", expand=True)
        canvas = tk.Canvas(frame, background="#808080", highlightthickness=0)
        self.preview_canvas = canvas
        vertical = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
        horizontal = ttk.Scrollbar(frame, orient="horizontal", command=canvas.xview)
        canvas.configure(yscrollcommand=vertical.set, xscrollcommand=horizontal.set)

        canvas.grid(row=0, column=0, sticky="nsew")
        vertical.grid(row=0, column=1, sticky="ns")
        horizontal.grid(row=1, column=0, sticky="ew")
        frame.rowconfigure(0, weight=1)
        frame.columnconfigure(0, weight=1)

        self.preview_status = ttk.Label(
            window,
            text="Подготовка предпросмотра...",
            padding=(10, 7),
            anchor="w",
        )
        self.preview_status.pack(fill="x")

    def _close_preview_window(self) -> None:
        """Корректно закрывает окно и отменяет ожидающую перерисовку."""
        if self.preview_after_id is not None:
            self.after_cancel(self.preview_after_id)
            self.preview_after_id = None
        if self.preview_window is not None:
            self.preview_window.destroy()
        self.preview_window = None
        self.preview_canvas = None
        self.preview_status = None
        self.preview_image = None

    def create_pdf(self) -> None:
        validated = self.validate_input()
        if validated is None:
            return
        path, date, _selected_page_settings = validated
        filename_date = self.date_for_filename(date)
        output_path = path.with_name(f"{path.stem}_{filename_date}.pdf")

        settings_by_page: list[dict[str, float | int]] = []
        for page_number in range(1, self.page_count + 1):
            try:
                settings_by_page.append(
                    self._parse_settings_values(self.page_settings[page_number])
                )
            except (ValueError, KeyError) as exc:
                self.current_page.set(page_number)
                messagebox.showerror(
                    APP_TITLE,
                    f"Ошибка в настройках страницы {page_number}:\n{exc}",
                )
                return

        if output_path.exists() and not messagebox.askyesno(
            APP_TITLE,
            f"Файл уже существует:\n{output_path}\n\nЗаменить его?",
        ):
            return

        document = None
        try:
            document = self.open_pdf(path)
            if document.page_count != len(settings_by_page):
                raise ValueError(
                    "Количество страниц PDF изменилось. Выберите файл заново."
                )
            for page_index, page in enumerate(document):
                self.add_dates(page, date, settings_by_page[page_index])

            # Сначала пишем во временный файл, затем атомарно заменяем результат.
            # Так уже существующий результат не повредится при ошибке сохранения.
            # Оставляем расширение .pdf: это совместимо и со старыми версиями PyMuPDF.
            temporary_path = output_path.with_name(f".{output_path.stem}.tmp.pdf")
            try:
                document.save(temporary_path, garbage=4, deflate=True)
                document.close()
                document = None
                temporary_path.replace(output_path)
            finally:
                if temporary_path.exists():
                    try:
                        temporary_path.unlink()
                    except OSError:
                        pass
        except Exception as exc:
            messagebox.showerror(APP_TITLE, f"Ошибка сохранения PDF:\n{exc}")
            return
        finally:
            if document is not None:
                document.close()

        messagebox.showinfo(
            APP_TITLE,
            f"PDF успешно сохранён:\n{output_path}",
        )

    def save_settings(self) -> None:
        pages_payload: dict[str, dict[str, float | int]] = {}
        for page_number in range(1, self.page_count + 1):
            try:
                pages_payload[str(page_number)] = self._parse_settings_values(
                    self.page_settings[page_number]
                )
            except (ValueError, KeyError) as exc:
                self.current_page.set(page_number)
                messagebox.showerror(
                    APP_TITLE,
                    f"Ошибка в настройках страницы {page_number}:\n{exc}",
                )
                return

        payload = {
            "format_version": 2,
            "page_count": self.page_count,
            "pages": pages_payload,
        }

        path = filedialog.asksaveasfilename(
            title="Сохранить настройки",
            defaultextension=".json",
            filetypes=[("JSON-файлы", "*.json")],
            initialfile="pdf_date_settings.json",
        )
        if not path:
            return

        try:
            with open(path, "w", encoding="utf-8") as file:
                json.dump(payload, file, ensure_ascii=False, indent=2)
        except OSError as exc:
            messagebox.showerror(APP_TITLE, f"Не удалось сохранить настройки:\n{exc}")
            return
        messagebox.showinfo(APP_TITLE, f"Настройки сохранены:\n{path}")

    def load_settings(self) -> None:
        path = filedialog.askopenfilename(
            title="Загрузить настройки",
            filetypes=[("JSON-файлы", "*.json"), ("Все файлы", "*.*")],
        )
        if not path:
            return

        try:
            with open(path, "r", encoding="utf-8") as file:
                loaded = json.load(file)
            if not isinstance(loaded, dict):
                raise ValueError("корневой элемент JSON должен быть объектом")

            if "pages" in loaded:
                pages_data = loaded["pages"]
                saved_page_count = int(loaded.get("page_count", 0))
                if not isinstance(pages_data, dict) or saved_page_count <= 0:
                    raise ValueError("неверный формат постраничных настроек")
                if self.settings_pdf_path and saved_page_count != self.page_count:
                    raise ValueError(
                        "количество страниц в JSON не совпадает с выбранным PDF"
                    )

                new_page_settings: dict[int, dict[str, str]] = {}
                for page_number in range(1, saved_page_count + 1):
                    page_data = pages_data.get(str(page_number))
                    if not isinstance(page_data, dict):
                        raise ValueError(
                            f"отсутствуют настройки страницы {page_number}"
                        )
                    missing = [key for key in DEFAULT_SETTINGS if key not in page_data]
                    if missing:
                        raise ValueError(
                            f"страница {page_number}: отсутствуют параметры "
                            f"{', '.join(missing)}"
                        )
                    raw_values = {
                        key: str(page_data[key]) for key in DEFAULT_SETTINGS
                    }
                    self._parse_settings_values(raw_values)
                    new_page_settings[page_number] = raw_values

                self.page_count = saved_page_count
                self.page_settings = new_page_settings
                self.page_spinbox.configure(to=saved_page_count)
                self.page_counter_text.set(f"из {saved_page_count}")
            else:
                # Поддержка старых JSON: один набор применяется ко всем страницам.
                missing = [key for key in DEFAULT_SETTINGS if key not in loaded]
                if missing:
                    raise ValueError(f"отсутствуют параметры: {', '.join(missing)}")
                raw_values = {key: str(loaded[key]) for key in DEFAULT_SETTINGS}
                self._parse_settings_values(raw_values)
                self.page_settings = {
                    page_number: raw_values.copy()
                    for page_number in range(1, self.page_count + 1)
                }

            selected_page = min(int(self.current_page.get()), self.page_count)
            self.loading_page_settings = True
            try:
                self.current_page.set(selected_page)
                self._load_page_settings_into_form(selected_page)
            finally:
                self.loading_page_settings = False
            self._schedule_live_preview()
        except (OSError, json.JSONDecodeError, ValueError, TypeError) as exc:
            messagebox.showerror(APP_TITLE, f"Не удалось загрузить настройки:\n{exc}")
            return

        messagebox.showinfo(APP_TITLE, "Настройки загружены.")


if __name__ == "__main__":
    app = PdfDateApp()
    app.mainloop()
