112 lines
3.2 KiB
Python
112 lines
3.2 KiB
Python
from collections.abc import Callable
|
|
from tkinter import messagebox
|
|
|
|
import customtkinter as ctk
|
|
|
|
from app.config import (
|
|
DANGER_COLOR,
|
|
DANGER_HOVER_COLOR,
|
|
PRIMARY_COLOR,
|
|
PRIMARY_HOVER_COLOR,
|
|
SUCCESS_COLOR,
|
|
WARNING_COLOR,
|
|
)
|
|
from app.services import FinanceService
|
|
from app.utils.formatters import format_money
|
|
|
|
|
|
class BaseAppFrame(ctk.CTkFrame):
|
|
"""Base class for application screens with shared UI helpers."""
|
|
|
|
def __init__(self, master, refresh_callback: Callable[[], None]) -> None:
|
|
super().__init__(master)
|
|
self.refresh_callback = refresh_callback
|
|
self.service = FinanceService()
|
|
self._busy = False
|
|
|
|
def run_safe(
|
|
self,
|
|
action: Callable[[], None],
|
|
button: ctk.CTkButton | None = None,
|
|
) -> None:
|
|
"""Run UI action with protection from repeated rapid clicks."""
|
|
if self._busy:
|
|
return
|
|
self._busy = True
|
|
if button is not None:
|
|
button.configure(state="disabled")
|
|
try:
|
|
action()
|
|
except ValueError as error:
|
|
messagebox.showerror("Ошибка ввода", str(error))
|
|
except Exception as error: # noqa: BLE001 - user-facing desktop app guard
|
|
messagebox.showerror("Ошибка", f"Действие не выполнено: {error}")
|
|
finally:
|
|
if button is not None:
|
|
self.after(350, lambda: button.configure(state="normal"))
|
|
self.after(350, self._unlock)
|
|
else:
|
|
self._unlock()
|
|
|
|
def _unlock(self) -> None:
|
|
self._busy = False
|
|
|
|
@staticmethod
|
|
def clear(container: ctk.CTkBaseClass) -> None:
|
|
for widget in container.winfo_children():
|
|
widget.destroy()
|
|
|
|
@staticmethod
|
|
def money(value: float | int | None) -> str:
|
|
return format_money(value)
|
|
|
|
@staticmethod
|
|
def title_label(master, text: str) -> ctk.CTkLabel:
|
|
return ctk.CTkLabel(
|
|
master,
|
|
text=text,
|
|
font=ctk.CTkFont(size=28, weight="bold"),
|
|
)
|
|
|
|
@staticmethod
|
|
def section_label(master, text: str) -> ctk.CTkLabel:
|
|
return ctk.CTkLabel(
|
|
master,
|
|
text=text,
|
|
font=ctk.CTkFont(size=18, weight="bold"),
|
|
)
|
|
|
|
@staticmethod
|
|
def subtitle_label(master, text: str) -> ctk.CTkLabel:
|
|
return ctk.CTkLabel(master, text=text, text_color="gray70")
|
|
|
|
@staticmethod
|
|
def primary_button(master, text: str, command) -> ctk.CTkButton:
|
|
return ctk.CTkButton(
|
|
master,
|
|
text=text,
|
|
height=42,
|
|
fg_color=PRIMARY_COLOR,
|
|
hover_color=PRIMARY_HOVER_COLOR,
|
|
command=command,
|
|
)
|
|
|
|
@staticmethod
|
|
def danger_button(master, text: str, command, width: int = 92) -> ctk.CTkButton:
|
|
return ctk.CTkButton(
|
|
master,
|
|
text=text,
|
|
width=width,
|
|
fg_color=DANGER_COLOR,
|
|
hover_color=DANGER_HOVER_COLOR,
|
|
command=command,
|
|
)
|
|
|
|
@staticmethod
|
|
def status_color(value: float) -> str:
|
|
if value >= 1:
|
|
return DANGER_COLOR
|
|
if value >= 0.8:
|
|
return WARNING_COLOR
|
|
return SUCCESS_COLOR
|