Десктопное приложение на PySide6. Вся база — один JSON-документ, зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит в data/vault.fmdb и раз в час уезжает в этот репозиторий. Основное: - партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж по партиям и прогресс покрытия к сроку; - типы выбытия: розница, другу по себестоимости, съел сам, подарок, списание — съеденное вычитается из прибыли, за него платить всё равно; - долги контрагентов с частичными оплатами; - номенклатура с историей изменения цен; - журнал изменений внутри базы: git хранит непрозрачные снимки, поэтому настоящая история ведётся здесь. Синхронизация коммитит только путь базы и схлопывает часовые пуши в один коммит на день через amend + force-with-lease. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
5.2 KiB
Python
182 lines
5.2 KiB
Python
"""Тёмная тема.
|
||
|
||
Палитра задаётся через QPalette, а не только через QSS: иначе системные
|
||
диалоги и всплывающие подсказки остались бы светлыми и резали глаза.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Qt
|
||
from PySide6.QtGui import QColor, QPalette
|
||
|
||
BG = "#1c1f24"
|
||
BG_ALT = "#23272e"
|
||
SURFACE = "#2b3038"
|
||
BORDER = "#3a4048"
|
||
TEXT = "#e6e8ea"
|
||
TEXT_DIM = "#98a0aa"
|
||
ACCENT = "#4c9aff"
|
||
|
||
OK = "#4caf7d"
|
||
WARN = "#d9a441"
|
||
DANGER = "#e0605e"
|
||
MUTED = "#6c757d"
|
||
|
||
STATUS_COLORS = {
|
||
"settled": OK,
|
||
"open": ACCENT,
|
||
"due_soon": WARN,
|
||
"overdue": DANGER,
|
||
}
|
||
|
||
|
||
def apply(app) -> None:
|
||
app.setStyle("Fusion")
|
||
|
||
palette = QPalette()
|
||
palette.setColor(QPalette.Window, QColor(BG))
|
||
palette.setColor(QPalette.WindowText, QColor(TEXT))
|
||
palette.setColor(QPalette.Base, QColor(BG_ALT))
|
||
palette.setColor(QPalette.AlternateBase, QColor(SURFACE))
|
||
palette.setColor(QPalette.Text, QColor(TEXT))
|
||
palette.setColor(QPalette.Button, QColor(SURFACE))
|
||
palette.setColor(QPalette.ButtonText, QColor(TEXT))
|
||
palette.setColor(QPalette.ToolTipBase, QColor(SURFACE))
|
||
palette.setColor(QPalette.ToolTipText, QColor(TEXT))
|
||
palette.setColor(QPalette.Highlight, QColor(ACCENT))
|
||
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
|
||
palette.setColor(QPalette.PlaceholderText, QColor(TEXT_DIM))
|
||
palette.setColor(QPalette.Link, QColor(ACCENT))
|
||
palette.setColor(QPalette.Disabled, QPalette.Text, QColor(MUTED))
|
||
palette.setColor(QPalette.Disabled, QPalette.ButtonText, QColor(MUTED))
|
||
palette.setColor(QPalette.Disabled, QPalette.WindowText, QColor(MUTED))
|
||
app.setPalette(palette)
|
||
|
||
app.setStyleSheet(QSS)
|
||
|
||
|
||
QSS = f"""
|
||
QWidget {{
|
||
font-size: 13px;
|
||
}}
|
||
|
||
QLabel[role="h1"] {{ font-size: 21px; font-weight: 600; }}
|
||
QLabel[role="h2"] {{ font-size: 15px; font-weight: 600; }}
|
||
QLabel[role="dim"] {{ color: {TEXT_DIM}; }}
|
||
QLabel[role="metric"] {{ font-size: 24px; font-weight: 600; }}
|
||
|
||
QFrame[role="card"] {{
|
||
background: {SURFACE};
|
||
border: 1px solid {BORDER};
|
||
border-radius: 8px;
|
||
}}
|
||
|
||
QFrame[role="banner"] {{
|
||
background: #3a2f1c;
|
||
border: 1px solid {WARN};
|
||
border-radius: 6px;
|
||
}}
|
||
|
||
QListWidget#sidebar {{
|
||
background: {BG_ALT};
|
||
border: none;
|
||
border-right: 1px solid {BORDER};
|
||
outline: none;
|
||
padding-top: 8px;
|
||
}}
|
||
QListWidget#sidebar::item {{
|
||
padding: 10px 16px;
|
||
margin: 1px 6px;
|
||
border-radius: 6px;
|
||
color: {TEXT_DIM};
|
||
}}
|
||
QListWidget#sidebar::item:selected {{
|
||
background: {SURFACE};
|
||
color: {TEXT};
|
||
font-weight: 600;
|
||
}}
|
||
QListWidget#sidebar::item:hover {{ color: {TEXT}; }}
|
||
|
||
QPushButton {{
|
||
background: {SURFACE};
|
||
border: 1px solid {BORDER};
|
||
border-radius: 6px;
|
||
padding: 6px 14px;
|
||
}}
|
||
QPushButton:hover {{ border-color: {ACCENT}; }}
|
||
QPushButton:disabled {{ color: {MUTED}; border-color: {BORDER}; }}
|
||
QPushButton[role="primary"] {{
|
||
background: {ACCENT};
|
||
border-color: {ACCENT};
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}}
|
||
QPushButton[role="primary"]:hover {{ background: #5aa6ff; }}
|
||
QPushButton[role="danger"]:hover {{ border-color: {DANGER}; color: {DANGER}; }}
|
||
|
||
QLineEdit, QDateEdit, QComboBox, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit {{
|
||
background: {BG_ALT};
|
||
border: 1px solid {BORDER};
|
||
border-radius: 6px;
|
||
padding: 5px 8px;
|
||
selection-background-color: {ACCENT};
|
||
}}
|
||
QLineEdit:focus, QDateEdit:focus, QComboBox:focus,
|
||
QSpinBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus {{
|
||
border-color: {ACCENT};
|
||
}}
|
||
QComboBox::drop-down {{ border: none; width: 18px; }}
|
||
QComboBox QAbstractItemView {{
|
||
background: {BG_ALT};
|
||
border: 1px solid {BORDER};
|
||
selection-background-color: {ACCENT};
|
||
}}
|
||
|
||
QTableWidget, QTreeWidget {{
|
||
background: {BG_ALT};
|
||
alternate-background-color: {SURFACE};
|
||
border: 1px solid {BORDER};
|
||
border-radius: 6px;
|
||
gridline-color: {BORDER};
|
||
selection-background-color: {ACCENT};
|
||
}}
|
||
QHeaderView::section {{
|
||
background: {SURFACE};
|
||
color: {TEXT_DIM};
|
||
border: none;
|
||
border-bottom: 1px solid {BORDER};
|
||
padding: 7px 8px;
|
||
font-weight: 600;
|
||
}}
|
||
QTableWidget::item, QTreeWidget::item {{ padding: 4px 6px; }}
|
||
|
||
QProgressBar {{
|
||
background: {BG};
|
||
border: 1px solid {BORDER};
|
||
border-radius: 6px;
|
||
height: 14px;
|
||
text-align: center;
|
||
color: {TEXT};
|
||
}}
|
||
QProgressBar::chunk {{ border-radius: 5px; }}
|
||
|
||
QStatusBar {{ border-top: 1px solid {BORDER}; }}
|
||
QStatusBar::item {{ border: none; }}
|
||
|
||
QScrollBar:vertical {{ background: transparent; width: 10px; margin: 0; }}
|
||
QScrollBar::handle:vertical {{ background: {BORDER}; border-radius: 5px; min-height: 30px; }}
|
||
QScrollBar::handle:vertical:hover {{ background: {MUTED}; }}
|
||
QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; width: 0; }}
|
||
QScrollBar:horizontal {{ background: transparent; height: 10px; }}
|
||
QScrollBar::handle:horizontal {{ background: {BORDER}; border-radius: 5px; min-width: 30px; }}
|
||
|
||
QTabWidget::pane {{ border: 1px solid {BORDER}; border-radius: 6px; }}
|
||
QTabBar::tab {{
|
||
background: transparent;
|
||
padding: 7px 14px;
|
||
color: {TEXT_DIM};
|
||
border-bottom: 2px solid transparent;
|
||
}}
|
||
QTabBar::tab:selected {{ color: {TEXT}; border-bottom-color: {ACCENT}; }}
|
||
"""
|