diff --git a/README.md b/README.md index 8d0a6e4..b23c9c7 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ > **AI-first API testing desktop client** - built with Python + PyQt6. > Specialised for the [EKIKA Odoo API Framework](https://apps.odoo.com/apps/modules/19.0/api_framework), but works with any REST, GraphQL, or WebSocket API. +![alt text](assets/app-ss.png) + --- ## Features diff --git a/app/core/fonts.py b/app/core/fonts.py new file mode 100644 index 0000000..8b73cd7 --- /dev/null +++ b/app/core/fonts.py @@ -0,0 +1,24 @@ +"""Font loader - registers bundled Quicksand and Open Sans with Qt.""" +import os +from PyQt6.QtGui import QFontDatabase + +_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets", "fonts") + +_FONT_FILES = [ + "Quicksand-Regular.ttf", + "Quicksand-Medium.ttf", + "Quicksand-SemiBold.ttf", + "Quicksand-Bold.ttf", + "OpenSans-Regular.ttf", + "OpenSans-SemiBold.ttf", + "OpenSans-Bold.ttf", +] + + +def load_fonts() -> None: + """Register all bundled fonts with QFontDatabase. Call once after QApplication is created.""" + fonts_dir = os.path.normpath(_ASSETS_DIR) + for filename in _FONT_FILES: + path = os.path.join(fonts_dir, filename) + if os.path.exists(path): + QFontDatabase.addApplicationFont(path) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 5efcf3d..958821c 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -1,20 +1,23 @@ """APIClient - Agent - Main Window.""" +import os from PyQt6.QtWidgets import ( QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget, QInputDialog, QMessageBox, QFileDialog, QApplication ) from PyQt6.QtCore import Qt, QThread, pyqtSignal -from PyQt6.QtGui import QKeySequence, QShortcut +from PyQt6.QtGui import QKeySequence, QShortcut, QPixmap from app.ui.tabs_manager import TabsManager from app.ui.response_panel import ResponsePanel from app.ui.sidebar import CollectionsSidebar -from app.ui.theme import Colors, toggle as toggle_theme, is_dark +from app.ui.theme import Colors, toggle as toggle_theme, is_dark, zoom_in, zoom_out, zoom_reset, get_zoom from app.core import http_client, storage from app.core.test_runner import run_tests from app.models import HttpRequest +_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets") + APP_VERSION = "2.0.0" APP_NAME = "APIClient - Agent" @@ -50,22 +53,62 @@ class EnvBar(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setObjectName("envBar") - self.setFixedHeight(46) + self.setFixedHeight(48) layout = QHBoxLayout(self) - layout.setContentsMargins(16, 0, 16, 0) + layout.setContentsMargins(12, 0, 12, 0) layout.setSpacing(6) + # EKIKA logo + logo_path = os.path.join(_ASSETS_DIR, "ekika_logo.png") + if os.path.exists(logo_path): + logo_lbl = QLabel() + logo_lbl.setObjectName("ekikaLogo") + pix = QPixmap(logo_path) + logo_lbl.setPixmap(pix.scaledToHeight(26, Qt.TransformationMode.SmoothTransformation)) + logo_lbl.setToolTip("EKIKA") + layout.addWidget(logo_lbl) + # Thin separator + sep = QLabel("|") + sep.setObjectName("brandSub") + sep.setFixedWidth(12) + sep.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(sep) + brand = QLabel("APIClient") brand.setObjectName("brandName") - sub = QLabel("Agent") + sub = QLabel("- Agent") sub.setObjectName("brandSub") layout.addWidget(brand) layout.addWidget(sub) layout.addStretch() + # Zoom controls + zoom_out_btn = QPushButton("-") + zoom_out_btn.setObjectName("zoomBtn") + zoom_out_btn.setFixedSize(24, 24) + zoom_out_btn.setToolTip("Zoom Out (Ctrl+-)") + layout.addWidget(zoom_out_btn) + self.zoom_out_btn = zoom_out_btn + + self.zoom_label = QLabel("100%") + self.zoom_label.setObjectName("zoomLabel") + self.zoom_label.setFixedWidth(42) + self.zoom_label.setToolTip("Current zoom level (Ctrl+0 to reset)") + layout.addWidget(self.zoom_label) + + zoom_in_btn = QPushButton("+") + zoom_in_btn.setObjectName("zoomBtn") + zoom_in_btn.setFixedSize(24, 24) + zoom_in_btn.setToolTip("Zoom In (Ctrl+=)") + layout.addWidget(zoom_in_btn) + self.zoom_in_btn = zoom_in_btn + + # Thin gap + layout.addSpacing(8) + env_label = QLabel("ENV") env_label.setObjectName("envChip") layout.addWidget(env_label) @@ -119,6 +162,8 @@ class MainWindow(QMainWindow): self.env_bar.manage_btn.clicked.connect(self._open_env_dialog) self.env_bar.theme_btn.clicked.connect(self._toggle_theme) self.env_bar.ai_btn.clicked.connect(self._toggle_ai_chat) + self.env_bar.zoom_in_btn.clicked.connect(self._zoom_in) + self.env_bar.zoom_out_btn.clicked.connect(self._zoom_out) root_layout.addWidget(self.env_bar) splitter = QSplitter(Qt.Orientation.Horizontal) @@ -203,6 +248,10 @@ class MainWindow(QMainWindow): view_m = mb.addMenu("View") view_m.addAction("Search Requests", self._open_search).setShortcut("Ctrl+F") view_m.addAction("Toggle Theme", self._toggle_theme) + view_m.addSeparator() + view_m.addAction("Zoom In", self._zoom_in).setShortcut("Ctrl+=") + view_m.addAction("Zoom Out", self._zoom_out).setShortcut("Ctrl+-") + view_m.addAction("Reset Zoom", self._zoom_reset).setShortcut("Ctrl+0") tools_m = mb.addMenu("Tools") tools_m.addAction("Environments…", self._open_env_dialog).setShortcut("Ctrl+E") @@ -227,10 +276,14 @@ class MainWindow(QMainWindow): QShortcut(QKeySequence("Ctrl+W"), self, self.tabs_manager.close_current_tab) QShortcut(QKeySequence("Ctrl+S"), self, self._save_to_collection) QShortcut(QKeySequence("Ctrl+F"), self, self._open_search) - QShortcut(QKeySequence("Ctrl+E"), self, self._open_env_dialog) - QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat) + QShortcut(QKeySequence("Ctrl+E"), self, self._open_env_dialog) + QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat) QShortcut(QKeySequence("Escape"), self, self._cancel_request) QShortcut(QKeySequence("Ctrl+Q"), self, self.close) + QShortcut(QKeySequence("Ctrl+="), self, self._zoom_in) + QShortcut(QKeySequence("Ctrl++"), self, self._zoom_in) + QShortcut(QKeySequence("Ctrl+-"), self, self._zoom_out) + QShortcut(QKeySequence("Ctrl+0"), self, self._zoom_reset) # ── Environment ─────────────────────────────────────────────────────────── @@ -443,6 +496,23 @@ class MainWindow(QMainWindow): elif atype == "test": rp.apply_test_script(content) + # ── Zoom ────────────────────────────────────────────────────────────────── + + def _zoom_in(self): + level = zoom_in(QApplication.instance()) + self._update_zoom_label(level) + + def _zoom_out(self): + level = zoom_out(QApplication.instance()) + self._update_zoom_label(level) + + def _zoom_reset(self): + level = zoom_reset(QApplication.instance()) + self._update_zoom_label(level) + + def _update_zoom_label(self, level: float): + self.env_bar.zoom_label.setText(f"{round(level * 100)}%") + # ── Theme ───────────────────────────────────────────────────────────────── def _toggle_theme(self): diff --git a/app/ui/theme.py b/app/ui/theme.py index 24eddd0..51c7f71 100644 --- a/app/ui/theme.py +++ b/app/ui/theme.py @@ -3,6 +3,8 @@ APIClient - Agent - Central Theme Engine All styling lives here in the global QSS. UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors. Only truly dynamic values (per-request method color, status badge) stay inline. + +Fonts: Quicksand (headings/brand/UI labels) + Open Sans (body text/inputs) """ from PyQt6.QtGui import QColor, QPalette from PyQt6.QtWidgets import QApplication @@ -100,6 +102,17 @@ class LightColors: Colors = DarkColors _is_dark = True +# ── Zoom level (1.0 = 100%) ─────────────────────────────────────────────────── +_zoom = 1.0 +_ZOOM_MIN = 0.7 +_ZOOM_MAX = 1.8 +_ZOOM_STEP = 0.1 + +# Font families +_UI_FONT = '"Quicksand", "Segoe UI", "SF Pro Text", sans-serif' +_BODY_FONT = '"Open Sans", "Segoe UI", "SF Pro Text", sans-serif' +_MONO_FONT = '"JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace' + def method_color(method: str) -> str: return { @@ -121,8 +134,12 @@ def status_color(code: int) -> str: return Colors.STATUS_5XX +def _z(size: float) -> str: + """Scale a font size by current zoom level and return px string.""" + return f"{round(size * _zoom)}px" + + # ── Global Stylesheet ───────────────────────────────────────────────────────── -# Everything static lives here. Object names are the API between theme and UI. def _build_stylesheet(C) -> str: return f""" @@ -133,8 +150,8 @@ def _build_stylesheet(C) -> str: QWidget {{ background-color: {C.BG_MAIN}; color: {C.TEXT_PRIMARY}; - font-family: "Segoe UI", "SF Pro Text", "Inter", "Helvetica Neue", sans-serif; - font-size: 13px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; border: none; outline: none; }} @@ -178,6 +195,9 @@ QMenuBar {{ border-bottom: 1px solid {C.BORDER}; padding: 2px 4px; spacing: 4px; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 600; }} QMenuBar::item {{ background: transparent; padding: 4px 10px; border-radius: 4px; @@ -191,6 +211,8 @@ QMenu {{ border: 1px solid {C.BORDER}; border-radius: 6px; padding: 4px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QMenu::item {{ padding: 7px 28px 7px 12px; border-radius: 4px; }} QMenu::item:selected {{ background-color: {C.BG_HOVER}; }} @@ -206,6 +228,8 @@ QLineEdit {{ border: 1px solid {C.BORDER}; border-radius: 5px; padding: 6px 10px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; selection-background-color: {C.ACCENT}; }} QLineEdit:focus {{ @@ -224,6 +248,8 @@ QTextEdit, QPlainTextEdit {{ border: 1px solid {C.BORDER}; border-radius: 5px; padding: 6px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; selection-background-color: {C.ACCENT}; }} QTextEdit:focus, QPlainTextEdit:focus {{ @@ -236,6 +262,8 @@ QSpinBox {{ border: 1px solid {C.BORDER}; border-radius: 5px; padding: 5px 8px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; }} QSpinBox:focus {{ border-color: {C.BORDER_FOCUS}; }} QSpinBox::up-button, QSpinBox::down-button {{ @@ -257,6 +285,8 @@ QComboBox {{ border-radius: 5px; padding: 5px 10px; min-width: 80px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; }} QComboBox:hover {{ border-color: {C.BORDER_FOCUS}; }} QComboBox:focus {{ border-color: {C.BORDER_FOCUS}; }} @@ -270,6 +300,8 @@ QComboBox QAbstractItemView {{ selection-background-color: {C.BG_SELECTED}; outline: none; padding: 2px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; }} /* ════════════════════════════════════════════════════════ @@ -279,6 +311,8 @@ QCheckBox {{ color: {C.TEXT_SECONDARY}; spacing: 6px; background: transparent; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QCheckBox::indicator {{ width: 14px; height: 14px; @@ -299,7 +333,9 @@ QPushButton {{ border: 1px solid {C.BORDER}; border-radius: 5px; padding: 6px 14px; - font-weight: 500; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 600; }} QPushButton:hover {{ background-color: {C.BG_HOVER}; @@ -312,7 +348,9 @@ QPushButton#accent {{ background-color: {C.ACCENT}; color: #FFFFFF; border: none; - font-weight: 600; + font-family: {_UI_FONT}; + font-weight: 700; + font-size: {_z(12)}; }} QPushButton#accent:hover {{ background-color: {C.ACCENT_HOVER}; }} QPushButton#accent:pressed {{ background-color: {C.ACCENT_PRESSED}; }} @@ -327,6 +365,9 @@ QPushButton#ghost {{ color: {C.TEXT_SECONDARY}; padding: 4px 8px; border-radius: 4px; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 600; }} QPushButton#ghost:hover {{ color: {C.TEXT_PRIMARY}; @@ -338,6 +379,9 @@ QPushButton#danger {{ background-color: transparent; color: {C.ERROR}; border: 1px solid {C.ERROR}; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 600; }} QPushButton#danger:hover {{ background-color: {C.ACCENT_SUBTLE}; }} @@ -347,8 +391,9 @@ QPushButton#sendBtn {{ border: none; border-radius: 6px; padding: 8px 22px; + font-family: {_UI_FONT}; font-weight: 700; - font-size: 13px; + font-size: {_z(13)}; letter-spacing: 0.3px; }} QPushButton#sendBtn:hover {{ background-color: {C.ACCENT_HOVER}; }} @@ -358,6 +403,22 @@ QPushButton#sendBtn:disabled {{ color: {C.TEXT_MUTED}; }} +QPushButton#zoomBtn {{ + background: transparent; + border: none; + color: {C.TEXT_MUTED}; + padding: 2px 5px; + border-radius: 4px; + font-family: {_UI_FONT}; + font-size: {_z(14)}; + font-weight: 700; +}} +QPushButton#zoomBtn:hover {{ + color: {C.TEXT_PRIMARY}; + background-color: {C.BG_HOVER}; +}} +QPushButton#zoomBtn:pressed {{ background-color: {C.BG_SELECTED}; }} + /* ════════════════════════════════════════════════════════ TABS ════════════════════════════════════════════════════════ */ @@ -372,8 +433,9 @@ QTabBar::tab {{ border: none; border-bottom: 2px solid transparent; padding: 8px 16px; - font-size: 12px; - font-weight: 500; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 600; }} QTabBar::tab:selected {{ color: {C.TEXT_PRIMARY}; @@ -406,8 +468,9 @@ QTabWidget#workspaceTabs QTabBar::tab {{ border: none; border-right: 1px solid {C.BORDER}; padding: 10px 20px; - font-size: 12px; - font-weight: 600; + font-family: {_UI_FONT}; + font-size: {_z(12)}; + font-weight: 700; border-bottom: none; border-top: 2px solid transparent; }} @@ -432,6 +495,8 @@ QTableWidget {{ gridline-color: {C.BORDER}; selection-background-color: {C.BG_SELECTED}; selection-color: {C.TEXT_PRIMARY}; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QTableWidget::item {{ padding: 5px 8px; @@ -448,8 +513,9 @@ QHeaderView::section {{ border-bottom: 1px solid {C.BORDER}; border-right: 1px solid {C.BORDER}; padding: 6px 8px; - font-size: 11px; - font-weight: 600; + font-family: {_UI_FONT}; + font-size: {_z(11)}; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }} @@ -464,6 +530,8 @@ QTreeWidget {{ border: none; outline: none; show-decoration-selected: 1; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QTreeWidget::item {{ padding: 4px 4px; @@ -485,6 +553,8 @@ QListWidget {{ border: 1px solid {C.BORDER}; border-radius: 5px; outline: none; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QListWidget::item {{ padding: 8px 10px; border-radius: 3px; }} QListWidget::item:selected {{ @@ -504,7 +574,8 @@ QListWidget#sidebarList::item {{ padding: 10px 14px; border-bottom: 1px solid {C.BORDER}; border-radius: 0; - font-size: 13px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QListWidget#sidebarList::item:selected {{ background: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY}; @@ -518,7 +589,8 @@ QStatusBar {{ background-color: {C.BG_DARKEST}; color: {C.TEXT_SECONDARY}; border-top: 1px solid {C.BORDER}; - font-size: 11px; + font-family: {_BODY_FONT}; + font-size: {_z(11)}; padding: 0 8px; }} QStatusBar::item {{ border: none; }} @@ -547,8 +619,9 @@ QGroupBox {{ border-radius: 6px; margin-top: 8px; padding: 8px; - font-size: 11px; - font-weight: 600; + font-family: {_UI_FONT}; + font-size: {_z(11)}; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }} @@ -565,7 +638,8 @@ QToolTip {{ border: 1px solid {C.BORDER}; border-radius: 4px; padding: 5px 8px; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} /* ════════════════════════════════════════════════════════ @@ -584,7 +658,7 @@ QFrame[frameShape="4"], QFrame[frameShape="5"] {{ }} /* ════════════════════════════════════════════════════════ - ── NAMED WIDGET RULES (setObjectName API) ── + -- NAMED WIDGET RULES (setObjectName API) -- ════════════════════════════════════════════════════════ */ /* Top brand / env bar */ @@ -594,24 +668,36 @@ QWidget#envBar {{ }} QLabel#brandName {{ color: {C.ACCENT}; - font-size: 15px; + font-family: {_UI_FONT}; + font-size: {_z(15)}; font-weight: 800; letter-spacing: 2px; background: transparent; }} QLabel#brandSub {{ color: {C.TEXT_MUTED}; - font-size: 11px; - font-weight: 500; + font-family: {_UI_FONT}; + font-size: {_z(11)}; + font-weight: 600; background: transparent; }} QLabel#envChip {{ color: {C.TEXT_MUTED}; - font-size: 10px; + font-family: {_UI_FONT}; + font-size: {_z(10)}; font-weight: 700; letter-spacing: 1px; background: transparent; }} +QLabel#zoomLabel {{ + color: {C.TEXT_MUTED}; + font-family: {_UI_FONT}; + font-size: {_z(10)}; + font-weight: 600; + background: transparent; + min-width: 36px; + qproperty-alignment: AlignCenter; +}} /* Sidebar */ QWidget#sidebar {{ @@ -636,8 +722,8 @@ QLineEdit#urlBar {{ border: 1.5px solid {C.BORDER}; border-radius: 6px; padding: 8px 12px; - font-size: 13px; - font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; + font-family: {_MONO_FONT}; + font-size: {_z(13)}; color: {C.TEXT_PRIMARY}; }} QLineEdit#urlBar:focus {{ @@ -647,8 +733,9 @@ QLineEdit#urlBar:focus {{ /* Method combo (color set inline per method, only layout here) */ QComboBox#methodCombo {{ + font-family: {_UI_FONT}; font-weight: 800; - font-size: 12px; + font-size: {_z(12)}; border-radius: 6px; padding: 8px 10px; min-width: 100px; @@ -677,14 +764,16 @@ QWidget#responseBar {{ }} QLabel#responseTitle {{ color: {C.TEXT_MUTED}; - font-size: 10px; + font-family: {_UI_FONT}; + font-size: {_z(10)}; font-weight: 700; letter-spacing: 1.2px; background: transparent; }} QLabel#metaLabel {{ color: {C.TEXT_MUTED}; - font-size: 11px; + font-family: {_BODY_FONT}; + font-size: {_z(11)}; background: transparent; padding: 0 6px; }} @@ -708,26 +797,30 @@ QWidget#panelBody {{ /* Labels inside panels */ QLabel#panelTitle {{ - font-size: 14px; + font-family: {_UI_FONT}; + font-size: {_z(14)}; font-weight: 700; color: {C.TEXT_PRIMARY}; background: transparent; }} QLabel#sectionLabel {{ color: {C.TEXT_MUTED}; - font-size: 10px; + font-family: {_UI_FONT}; + font-size: {_z(10)}; font-weight: 700; letter-spacing: 1px; background: transparent; }} QLabel#hintText {{ color: {C.TEXT_MUTED}; - font-size: 11px; + font-family: {_BODY_FONT}; + font-size: {_z(11)}; background: transparent; }} QLabel#fieldLabel {{ color: {C.TEXT_SECONDARY}; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; background: transparent; }} @@ -737,8 +830,8 @@ QTextEdit#codeEditor {{ color: {C.TEXT_PRIMARY}; border: none; padding: 8px; - font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; - font-size: 11px; + font-family: {_MONO_FONT}; + font-size: {_z(11)}; }} /* Loading overlay */ @@ -747,7 +840,8 @@ QWidget#loadingOverlay {{ }} QLabel#loadingLabel {{ color: {C.TEXT_MUTED}; - font-size: 13px; + font-family: {_BODY_FONT}; + font-size: {_z(13)}; background: transparent; }} @@ -757,7 +851,8 @@ QLineEdit#searchBar {{ border: 1px solid {C.BORDER}; border-radius: 4px; padding: 4px 8px; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; color: {C.TEXT_PRIMARY}; }} QLineEdit#searchBar:focus {{ border-color: {C.BORDER_FOCUS}; }} @@ -768,7 +863,8 @@ QLineEdit#filterInput {{ border: 1px solid {C.BORDER}; border-radius: 4px; padding: 5px 8px; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; color: {C.TEXT_PRIMARY}; }} QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }} @@ -776,19 +872,22 @@ QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }} /* WebSocket / Mock status indicator labels */ QLabel#statusOk {{ color: {C.SUCCESS}; - font-size: 12px; + font-family: {_UI_FONT}; + font-size: {_z(12)}; font-weight: 600; background: transparent; }} QLabel#statusWarn {{ color: {C.WARNING}; - font-size: 12px; + font-family: {_UI_FONT}; + font-size: {_z(12)}; font-weight: 600; background: transparent; }} QLabel#statusErr {{ color: {C.ERROR}; - font-size: 12px; + font-family: {_UI_FONT}; + font-size: {_z(12)}; font-weight: 600; background: transparent; }} @@ -796,6 +895,8 @@ QLabel#statusErr {{ /* Auth "none" hint */ QLabel#authNone {{ color: {C.TEXT_MUTED}; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; padding: 12px; background: transparent; }} @@ -811,7 +912,7 @@ QPushButton#tabCloseBtn {{ border: none; border-radius: 3px; color: {C.TEXT_MUTED}; - font-size: 14px; + font-size: {_z(14)}; font-weight: 700; padding: 0; }} @@ -830,15 +931,17 @@ QTextEdit#aiOutput {{ border: 1px solid {C.BORDER}; border-radius: 5px; padding: 8px; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; }} QLabel#aiStatusLabel {{ color: {C.TEXT_MUTED}; - font-size: 11px; + font-family: {_BODY_FONT}; + font-size: {_z(11)}; background: transparent; }} -/* ── AI Chat Panel ─────────────────────────────────────── */ +/* -- AI Chat Panel ─────────────────────────────────────── */ QWidget#aiChatPanel {{ background-color: {C.BG_SIDEBAR}; border-left: 1px solid {C.BORDER}; @@ -849,7 +952,8 @@ QWidget#aiChatHeader {{ }} QLabel#aiChatTitle {{ color: {C.ACCENT}; - font-size: 13px; + font-family: {_UI_FONT}; + font-size: {_z(13)}; font-weight: 700; letter-spacing: 0.5px; background: transparent; @@ -871,7 +975,8 @@ QFrame#aiBubble {{ margin: 0px; }} QLabel#chatRoleLabel {{ - font-size: 10px; + font-family: {_UI_FONT}; + font-size: {_z(10)}; font-weight: 700; color: {C.TEXT_MUTED}; background: transparent; @@ -881,7 +986,8 @@ QLabel#chatRoleLabel {{ QLabel#chatMessageText {{ color: {C.TEXT_PRIMARY}; background: transparent; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; line-height: 1.6; }} QWidget#chatInputArea {{ @@ -893,7 +999,8 @@ QTextEdit#chatInput {{ border: 1px solid {C.BORDER}; border-radius: 6px; padding: 6px 10px; - font-size: 12px; + font-family: {_BODY_FONT}; + font-size: {_z(12)}; color: {C.TEXT_PRIMARY}; }} QTextEdit#chatInput:focus {{ @@ -909,9 +1016,10 @@ QPushButton#qaBtn {{ border: 1px solid {C.BORDER}; border-radius: 10px; padding: 2px 8px; - font-size: 11px; + font-family: {_UI_FONT}; + font-size: {_z(11)}; color: {C.TEXT_SECONDARY}; - font-weight: 500; + font-weight: 600; }} QPushButton#qaBtn:hover {{ background-color: {C.BG_HOVER}; @@ -928,8 +1036,8 @@ QTextEdit#applyCode {{ background-color: transparent; border: none; padding: 4px; - font-size: 10px; - font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; + font-family: {_MONO_FONT}; + font-size: {_z(10)}; color: {C.TEXT_SECONDARY}; }} @@ -974,6 +1082,34 @@ def is_dark() -> bool: return _is_dark +def zoom_in(app: QApplication) -> float: + """Increase zoom one step. Returns new zoom level.""" + global _zoom + _zoom = min(_ZOOM_MAX, round(_zoom + _ZOOM_STEP, 1)) + app.setStyleSheet(_build_stylesheet(Colors)) + return _zoom + + +def zoom_out(app: QApplication) -> float: + """Decrease zoom one step. Returns new zoom level.""" + global _zoom + _zoom = max(_ZOOM_MIN, round(_zoom - _ZOOM_STEP, 1)) + app.setStyleSheet(_build_stylesheet(Colors)) + return _zoom + + +def zoom_reset(app: QApplication) -> float: + """Reset zoom to 100%. Returns new zoom level.""" + global _zoom + _zoom = 1.0 + app.setStyleSheet(_build_stylesheet(Colors)) + return _zoom + + +def get_zoom() -> float: + return _zoom + + def restyle(widget, obj_name: str) -> None: """Change a widget's objectName and force Qt to re-evaluate CSS rules.""" widget.setObjectName(obj_name) diff --git a/assets/app-ss.png b/assets/app-ss.png new file mode 100644 index 0000000..d5b5a66 Binary files /dev/null and b/assets/app-ss.png differ diff --git a/assets/ekika_logo.png b/assets/ekika_logo.png new file mode 100644 index 0000000..7a3f64b Binary files /dev/null and b/assets/ekika_logo.png differ diff --git a/assets/ekika_logo.webp b/assets/ekika_logo.webp new file mode 100644 index 0000000..3a8df98 Binary files /dev/null and b/assets/ekika_logo.webp differ diff --git a/assets/fonts/OpenSans-Bold.ttf b/assets/fonts/OpenSans-Bold.ttf new file mode 100644 index 0000000..1265fa4 Binary files /dev/null and b/assets/fonts/OpenSans-Bold.ttf differ diff --git a/assets/fonts/OpenSans-Regular.ttf b/assets/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..4afe506 Binary files /dev/null and b/assets/fonts/OpenSans-Regular.ttf differ diff --git a/assets/fonts/OpenSans-SemiBold.ttf b/assets/fonts/OpenSans-SemiBold.ttf new file mode 100644 index 0000000..de78228 Binary files /dev/null and b/assets/fonts/OpenSans-SemiBold.ttf differ diff --git a/assets/fonts/Quicksand-Bold.ttf b/assets/fonts/Quicksand-Bold.ttf new file mode 100644 index 0000000..0b723fa Binary files /dev/null and b/assets/fonts/Quicksand-Bold.ttf differ diff --git a/assets/fonts/Quicksand-Medium.ttf b/assets/fonts/Quicksand-Medium.ttf new file mode 100644 index 0000000..83e439f Binary files /dev/null and b/assets/fonts/Quicksand-Medium.ttf differ diff --git a/assets/fonts/Quicksand-Regular.ttf b/assets/fonts/Quicksand-Regular.ttf new file mode 100644 index 0000000..4a2b2ab Binary files /dev/null and b/assets/fonts/Quicksand-Regular.ttf differ diff --git a/assets/fonts/Quicksand-SemiBold.ttf b/assets/fonts/Quicksand-SemiBold.ttf new file mode 100644 index 0000000..42925ca Binary files /dev/null and b/assets/fonts/Quicksand-SemiBold.ttf differ diff --git a/main.py b/main.py index 42896ed..cbd4b1e 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import sys from PyQt6.QtWidgets import QApplication +from app.core.fonts import load_fonts from app.ui.theme import apply from app.ui.main_window import MainWindow @@ -11,6 +12,7 @@ if __name__ == "__main__": app.setApplicationName(APP_NAME) app.setApplicationVersion(APP_VERSION) app.setOrganizationName("EKIKA") + load_fonts() apply(app, dark=True) window = MainWindow() window.show()