diff --git a/app.py b/app.py new file mode 100644 index 0000000..e113ef3 --- /dev/null +++ b/app.py @@ -0,0 +1,10 @@ +import sys +from PySide6.QtWidgets import QApplication +from ui.main_window import MainWindow + + +def run_app(): + app = QApplication(sys.argv) + window = MainWindow(app) + window.show() + sys.exit(app.exec()) diff --git a/main.py b/main.py index cd80255..edcbfcd 100644 --- a/main.py +++ b/main.py @@ -1,373 +1,4 @@ -import sys -from PySide6.QtCore import Qt, QTimer, QSize -from PySide6.QtGui import QFont -from PySide6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QLabel, QPushButton, - QHBoxLayout, QVBoxLayout, QGridLayout, QStackedWidget, QSizePolicy -) - - -# ---------- Themes (QSS) ---------- -THEME_NIGHT = """ -/* Global */ -QWidget { background: #0B0E11; color: #E6EAF0; font-family: Inter, Roboto, Sans-Serif; } -QLabel { color: #E6EAF0; } -#TopBar, #BottomBar { background: #0F1318; border: 0px; } -#Divider { background: #1B2330; } - -QPushButton { border: 0px; } -QPushButton#IconChip { - background: #161A1F; - padding: 10px 14px; - border-radius: 14px; - color: #E6EAF0; - font-size: 16px; -} -QPushButton#IconChip:pressed { background: #1B2330; } - -QPushButton#NavBtn { - background: transparent; - padding: 10px 8px; - border-radius: 14px; - color: #8A93A6; - font-size: 14px; -} -QPushButton#NavBtn[active="true"] { - color: #E6EAF0; -} -QPushButton#NavBtn[active="true"] #NavUnderline { - background: #3A86FF; -} - -QWidget#Card { - background: #161A1F; - border-radius: 20px; -} -QLabel#CardTitle { color: #8A93A6; font-size: 16px; } -QLabel#CardValue { color: #E6EAF0; font-size: 34px; font-weight: 600; } -QLabel#CardSub { color: #8A93A6; font-size: 14px; } - -QPushButton#PrimaryBtn { - background: #3A86FF; - color: #0B0E11; - border-radius: 16px; - padding: 12px 16px; - font-size: 16px; - font-weight: 600; -} -QPushButton#PrimaryBtn:pressed { background: #2F6BE0; } -""" - -THEME_DAY = """ -/* Global */ -QWidget { background: #F4F6F8; color: #111827; font-family: Inter, Roboto, Sans-Serif; } -QLabel { color: #111827; } -#TopBar, #BottomBar { background: #FFFFFF; border: 0px; } -#Divider { background: #E5E7EB; } - -QPushButton { border: 0px; } -QPushButton#IconChip { - background: #F1F5F9; - padding: 10px 14px; - border-radius: 14px; - color: #111827; - font-size: 16px; -} -QPushButton#IconChip:pressed { background: #E2E8F0; } - -QPushButton#NavBtn { - background: transparent; - padding: 10px 8px; - border-radius: 14px; - color: #6B7280; - font-size: 14px; -} -QPushButton#NavBtn[active="true"] { - color: #111827; -} -QPushButton#NavBtn[active="true"] #NavUnderline { - background: #2563EB; -} - -QWidget#Card { - background: #FFFFFF; - border-radius: 20px; -} -QLabel#CardTitle { color: #6B7280; font-size: 16px; } -QLabel#CardValue { color: #111827; font-size: 34px; font-weight: 600; } -QLabel#CardSub { color: #6B7280; font-size: 14px; } - -QPushButton#PrimaryBtn { - background: #2563EB; - color: #FFFFFF; - border-radius: 16px; - padding: 12px 16px; - font-size: 16px; - font-weight: 600; -} -QPushButton#PrimaryBtn:pressed { background: #1D4ED8; } -""" - - -# ---------- Small UI helpers ---------- -def divider(height=1): - w = QWidget() - w.setObjectName("Divider") - w.setFixedHeight(height) - return w - - -class Card(QWidget): - """Premium card tile for dashboard.""" - def __init__(self, title: str, value: str, sub: str, icon_text: str = "◻"): - super().__init__() - self.setObjectName("Card") - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - root = QVBoxLayout(self) - root.setContentsMargins(18, 16, 18, 16) - root.setSpacing(10) - - top = QHBoxLayout() - top.setSpacing(10) - - lbl_title = QLabel(title) - lbl_title.setObjectName("CardTitle") - - icon = QLabel(icon_text) - icon.setAlignment(Qt.AlignCenter) - icon.setFixedSize(44, 44) - icon.setStyleSheet("background: rgba(58,134,255,0.12); border-radius: 14px;") - - top.addWidget(lbl_title, 1, Qt.AlignLeft | Qt.AlignVCenter) - top.addWidget(icon, 0, Qt.AlignRight | Qt.AlignVCenter) - - lbl_value = QLabel(value) - lbl_value.setObjectName("CardValue") - - lbl_sub = QLabel(sub) - lbl_sub.setObjectName("CardSub") - - root.addLayout(top) - root.addStretch(1) - root.addWidget(lbl_value) - root.addWidget(lbl_sub) - - # clickable feel - self.setCursor(Qt.PointingHandCursor) - - def mousePressEvent(self, event): - # заглушка клика по карточке - print(f"[Card] Click: {self.objectName()}") - super().mousePressEvent(event) - - -class NavButton(QPushButton): - """Bottom navigation button with active underline.""" - def __init__(self, text: str): - super().__init__() - self.setObjectName("NavBtn") - self.setCheckable(False) - self.setCursor(Qt.PointingHandCursor) - - self._wrap = QWidget() - wrap_layout = QVBoxLayout(self._wrap) - wrap_layout.setContentsMargins(0, 0, 0, 0) - wrap_layout.setSpacing(6) - - self._label = QLabel(text) - self._label.setAlignment(Qt.AlignCenter) - - self._underline = QWidget() - self._underline.setObjectName("NavUnderline") - self._underline.setFixedHeight(3) - self._underline.setFixedWidth(36) - self._underline.setStyleSheet("background: transparent; border-radius: 2px;") - - wrap_layout.addStretch(1) - wrap_layout.addWidget(self._label) - wrap_layout.addWidget(self._underline, 0, Qt.AlignHCenter) - wrap_layout.addStretch(1) - - btn_layout = QVBoxLayout(self) - btn_layout.setContentsMargins(0, 0, 0, 0) - btn_layout.addWidget(self._wrap) - - def set_active(self, active: bool): - self.setProperty("active", "true" if active else "false") - # important to re-polish to apply dynamic property styling: - self.style().unpolish(self) - self.style().polish(self) - - -# ---------- Screens ---------- -class HomeScreen(QWidget): - def __init__(self): - super().__init__() - root = QVBoxLayout(self) - root.setContentsMargins(18, 16, 18, 16) - root.setSpacing(16) - - hdr = QHBoxLayout() - title = QLabel("Dashboard") - title.setFont(QFont("", 22, 600)) - hint = QLabel("Premium UI • 1024×600") - hint.setStyleSheet("color: rgba(138,147,166,0.9);") - - hdr.addWidget(title, 0, Qt.AlignLeft | Qt.AlignVCenter) - hdr.addStretch(1) - hdr.addWidget(hint, 0, Qt.AlignRight | Qt.AlignVCenter) - - grid = QGridLayout() - grid.setContentsMargins(0, 0, 0, 0) - grid.setHorizontalSpacing(16) - grid.setVerticalSpacing(16) - - # 2×2 cards - grid.addWidget(Card("MEDIA", "No track", "Tap to open", "♫"), 0, 0) - grid.addWidget(Card("MAP / TRACK", "Idle", "Last trip: —", "⌁"), 0, 1) - grid.addWidget(Card("CAR STATUS", "OK", "OBD: not connected", "⛭"), 1, 0) - grid.addWidget(Card("CLOUD / YOUBLE", "Offline", "No sync yet", "☁"), 1, 1) - - root.addLayout(hdr) - root.addLayout(grid, 1) - - -class StubScreen(QWidget): - def __init__(self, title_text: str): - super().__init__() - root = QVBoxLayout(self) - root.setContentsMargins(18, 16, 18, 16) - root.setSpacing(12) - - title = QLabel(title_text) - title.setFont(QFont("", 22, 600)) - - sub = QLabel("Заглушка экрана. Тут будет функционал.") - sub.setStyleSheet("color: rgba(138,147,166,0.9);") - - root.addWidget(title) - root.addWidget(sub) - root.addStretch(1) - - -# ---------- Main Window ---------- -class MainWindow(QMainWindow): - def __init__(self, app: QApplication): - super().__init__() - self.app = app - self.is_night = True - - self.setWindowTitle("Car UI (Premium)") - self.setMinimumSize(QSize(1024, 600)) - self.showFullScreen() - - # Root layout container - central = QWidget() - outer = QVBoxLayout(central) - outer.setContentsMargins(0, 0, 0, 0) - outer.setSpacing(0) - - # Top bar - self.topbar = QWidget() - self.topbar.setObjectName("TopBar") - top = QHBoxLayout(self.topbar) - top.setContentsMargins(16, 10, 16, 10) - top.setSpacing(10) - - self.lbl_time = QLabel("--:--") - self.lbl_time.setFont(QFont("", 16, 600)) - - chip_bt = QPushButton("BT") - chip_bt.setObjectName("IconChip") - chip_wifi = QPushButton("Wi-Fi") - chip_wifi.setObjectName("IconChip") - chip_lte = QPushButton("LTE") - chip_lte.setObjectName("IconChip") - - self.lbl_vol = QLabel("VOL 45%") - self.lbl_vol.setStyleSheet("color: rgba(138,147,166,0.95); font-size: 14px;") - - self.btn_theme = QPushButton("🌙") - self.btn_theme.setObjectName("IconChip") - self.btn_theme.clicked.connect(self.toggle_theme) - - top.addWidget(self.lbl_time) - top.addSpacing(10) - top.addWidget(chip_bt) - top.addWidget(chip_wifi) - top.addWidget(chip_lte) - top.addStretch(1) - top.addWidget(self.lbl_vol) - top.addWidget(self.btn_theme) - - # Content stack - self.stack = QStackedWidget() - self.stack.addWidget(HomeScreen()) # 0 - self.stack.addWidget(StubScreen("Media")) # 1 - self.stack.addWidget(StubScreen("Car")) # 2 - self.stack.addWidget(StubScreen("Maps")) # 3 - self.stack.addWidget(StubScreen("Settings")) # 4 - - # Bottom bar (nav) - self.bottombar = QWidget() - self.bottombar.setObjectName("BottomBar") - bottom = QHBoxLayout(self.bottombar) - bottom.setContentsMargins(16, 8, 16, 8) - bottom.setSpacing(10) - - self.nav_buttons = [ - NavButton("HOME"), - NavButton("MEDIA"), - NavButton("CAR"), - NavButton("MAPS"), - NavButton("SETTINGS"), - ] - - for i, btn in enumerate(self.nav_buttons): - btn.clicked.connect(lambda checked=False, idx=i: self.go(idx)) - bottom.addWidget(btn, 1) - - outer.addWidget(self.topbar) - outer.addWidget(divider(1)) - outer.addWidget(self.stack, 1) - outer.addWidget(divider(1)) - outer.addWidget(self.bottombar) - - self.setCentralWidget(central) - - # timers - self._clock_timer = QTimer(self) - self._clock_timer.timeout.connect(self.update_time) - self._clock_timer.start(500) - - self.apply_theme() - self.go(0) - - def update_time(self): - from datetime import datetime - self.lbl_time.setText(datetime.now().strftime("%H:%M")) - - def apply_theme(self): - self.app.setStyleSheet(THEME_NIGHT if self.is_night else THEME_DAY) - self.btn_theme.setText("🌙" if self.is_night else "☀") - - def toggle_theme(self): - self.is_night = not self.is_night - self.apply_theme() - - def go(self, idx: int): - self.stack.setCurrentIndex(idx) - for i, b in enumerate(self.nav_buttons): - b.set_active(i == idx) - - -def main(): - app = QApplication(sys.argv) - win = MainWindow(app) - win.show() - sys.exit(app.exec()) - +from app import run_app if __name__ == "__main__": - main() + run_app() diff --git a/screens/media.py b/screens/__init__.py similarity index 100% rename from screens/media.py rename to screens/__init__.py diff --git a/screens/home.py b/screens/home.py index e69de29..8b5f472 100644 --- a/screens/home.py +++ b/screens/home.py @@ -0,0 +1,40 @@ +from PySide6.QtWidgets import QWidget, QVBoxLayout, QGridLayout, QLabel +from PySide6.QtGui import QFont +from ui.components.card import Card +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QLabel, QPushButton, + QHBoxLayout, QVBoxLayout, QGridLayout, QStackedWidget, QSizePolicy +) +from PySide6.QtCore import QSize +from PySide6.QtGui import QFont + +class HomeScreen(QWidget): + def __init__(self): + super().__init__() + root = QVBoxLayout(self) + root.setContentsMargins(18, 16, 18, 16) + root.setSpacing(16) + + hdr = QHBoxLayout() + title = QLabel("Dashboard") + title.setFont(QFont("", 22, 600)) + hint = QLabel("Premium UI • 1024×600") + hint.setStyleSheet("color: rgba(138,147,166,0.9);") + + hdr.addWidget(title) + hdr.addStretch(1) + hdr.addWidget(hint) + + grid = QGridLayout() + grid.setContentsMargins(0, 0, 0, 0) + grid.setHorizontalSpacing(16) + grid.setVerticalSpacing(16) + + # 2×2 cards + grid.addWidget(Card("MEDIA", "No track", "Tap to open", "♫"), 0, 0) + grid.addWidget(Card("MAP / TRACK", "Idle", "Last trip: —", "⌁"), 0, 1) + grid.addWidget(Card("CAR STATUS", "OK", "OBD: not connected", "⛭"), 1, 0) + grid.addWidget(Card("CLOUD / YOUBLE", "Offline", "No sync yet", "☁"), 1, 1) + + root.addLayout(hdr) + root.addLayout(grid, 1) diff --git a/screens/stub.py b/screens/stub.py new file mode 100644 index 0000000..cb19e1f --- /dev/null +++ b/screens/stub.py @@ -0,0 +1,20 @@ +from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout +from PySide6.QtGui import QFont + +class StubScreen(QWidget): + def __init__(self, title_text: str): + super().__init__() + root = QVBoxLayout(self) + root.setContentsMargins(18, 16, 18, 16) + root.setSpacing(12) + + title = QLabel(title_text) + title.setFont(QFont("", 22, 600)) + + sub = QLabel("Заглушка экрана. Тут будет функционал.") + sub.setStyleSheet("color: rgba(138,147,166,0.9);") + + root.addWidget(title) + root.addWidget(sub) + root.addStretch(1) + diff --git a/themes/__init__.py b/themes/__init__.py new file mode 100644 index 0000000..7890cf8 --- /dev/null +++ b/themes/__init__.py @@ -0,0 +1,2 @@ +from .day import THEME_DAY +from .night import THEME_NIGHT diff --git a/themes/day.py b/themes/day.py new file mode 100644 index 0000000..e0bdb30 --- /dev/null +++ b/themes/day.py @@ -0,0 +1,5 @@ +THEME_DAY = """ +QWidget { background: #F4F6F8; color: #111827; } +#TopBar, #BottomBar { background: #FFFFFF; } +#Divider { background: #E5E7EB; } +""" diff --git a/themes/night.py b/themes/night.py new file mode 100644 index 0000000..eb64d83 --- /dev/null +++ b/themes/night.py @@ -0,0 +1,5 @@ +THEME_NIGHT = """ +QWidget { background: #0B0E11; color: #E6EAF0; } +#TopBar, #BottomBar { background: #0F1318; } +#Divider { background: #1B2330; } +""" diff --git a/screens/radio.py b/ui/__init__.py similarity index 100% rename from screens/radio.py rename to ui/__init__.py diff --git a/screens/settings.py b/ui/bottom_bar.py similarity index 100% rename from screens/settings.py rename to ui/bottom_bar.py diff --git a/ui/components/__init__.py b/ui/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/components/card.py b/ui/components/card.py new file mode 100644 index 0000000..6cdf770 --- /dev/null +++ b/ui/components/card.py @@ -0,0 +1,46 @@ +from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QSizePolicy +from PySide6.QtCore import Qt + +class Card(QWidget): + """Premium card tile for dashboard.""" + def __init__(self, title: str, value: str, sub: str, icon_text: str = "◻"): + super().__init__() + self.setObjectName("Card") + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + root = QVBoxLayout(self) + root.setContentsMargins(18, 16, 18, 16) + root.setSpacing(10) + + top = QHBoxLayout() + top.setSpacing(10) + + lbl_title = QLabel(title) + lbl_title.setObjectName("CardTitle") + + icon = QLabel(icon_text) + icon.setAlignment(Qt.AlignCenter) + icon.setFixedSize(44, 44) + icon.setStyleSheet("background: rgba(58,134,255,0.12); border-radius: 14px;") + + top.addWidget(lbl_title, 1, Qt.AlignLeft | Qt.AlignVCenter) + top.addWidget(icon, 0, Qt.AlignRight | Qt.AlignVCenter) + + lbl_value = QLabel(value) + lbl_value.setObjectName("CardValue") + + lbl_sub = QLabel(sub) + lbl_sub.setObjectName("CardSub") + + root.addLayout(top) + root.addStretch(1) + root.addWidget(lbl_value) + root.addWidget(lbl_sub) + + # clickable feel + self.setCursor(Qt.PointingHandCursor) + + def mousePressEvent(self, event): + # заглушка клика по карточке + print(f"[Card] Click: {self.objectName()}") + super().mousePressEvent(event) diff --git a/ui/components/divider.py b/ui/components/divider.py new file mode 100644 index 0000000..f11869a --- /dev/null +++ b/ui/components/divider.py @@ -0,0 +1,7 @@ +from PySide6.QtWidgets import QWidget + +def divider(height=1): + w = QWidget() + w.setObjectName("Divider") + w.setFixedHeight(height) + return w diff --git a/ui/components/nav_button.py b/ui/components/nav_button.py new file mode 100644 index 0000000..f5043ca --- /dev/null +++ b/ui/components/nav_button.py @@ -0,0 +1,40 @@ +from PySide6.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget +from PySide6.QtCore import Qt + + +class NavButton(QPushButton): + """Bottom navigation button with active underline.""" + def __init__(self, text: str): + super().__init__() + self.setObjectName("NavBtn") + self.setCheckable(False) + self.setCursor(Qt.PointingHandCursor) + + self._wrap = QWidget() + wrap_layout = QVBoxLayout(self._wrap) + wrap_layout.setContentsMargins(0, 0, 0, 0) + wrap_layout.setSpacing(6) + + self._label = QLabel(text) + self._label.setAlignment(Qt.AlignCenter) + + self._underline = QWidget() + self._underline.setObjectName("NavUnderline") + self._underline.setFixedHeight(3) + self._underline.setFixedWidth(36) + self._underline.setStyleSheet("background: transparent; border-radius: 2px;") + + wrap_layout.addStretch(1) + wrap_layout.addWidget(self._label) + wrap_layout.addWidget(self._underline, 0, Qt.AlignHCenter) + wrap_layout.addStretch(1) + + btn_layout = QVBoxLayout(self) + btn_layout.setContentsMargins(0, 0, 0, 0) + btn_layout.addWidget(self._wrap) + + def set_active(self, active: bool): + self.setProperty("active", "true" if active else "false") + # important to re-polish to apply dynamic property styling: + self.style().unpolish(self) + self.style().polish(self) diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000..b03d766 --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,123 @@ +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QStackedWidget, QHBoxLayout, QLabel, QPushButton +from PySide6.QtCore import QTimer +from themes import THEME_DAY, THEME_NIGHT +from ui.components.divider import divider +from ui.components.nav_button import NavButton +from screens.home import HomeScreen +from screens.stub import StubScreen +from datetime import datetime +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QLabel, QPushButton, + QHBoxLayout, QVBoxLayout, QGridLayout, QStackedWidget, QSizePolicy +) +from PySide6.QtCore import QSize +from PySide6.QtGui import QFont + +class MainWindow(QMainWindow): + def __init__(self, app: QApplication): + super().__init__() + self.app = app + self.is_night = True + + self.setWindowTitle("Car UI (Premium)") + self.setMinimumSize(QSize(1024, 600)) + self.showFullScreen() + + # Root layout container + central = QWidget() + outer = QVBoxLayout(central) + outer.setContentsMargins(0, 0, 0, 0) + outer.setSpacing(0) + + # Top bar + self.topbar = QWidget() + self.topbar.setObjectName("TopBar") + top = QHBoxLayout(self.topbar) + top.setContentsMargins(16, 10, 16, 10) + top.setSpacing(10) + + self.lbl_time = QLabel("--:--") + self.lbl_time.setFont(QFont("", 16, 600)) + + chip_bt = QPushButton("BT") + chip_bt.setObjectName("IconChip") + chip_wifi = QPushButton("Wi-Fi") + chip_wifi.setObjectName("IconChip") + chip_lte = QPushButton("LTE") + chip_lte.setObjectName("IconChip") + + self.lbl_vol = QLabel("VOL 45%") + self.lbl_vol.setStyleSheet("color: rgba(138,147,166,0.95); font-size: 14px;") + + self.btn_theme = QPushButton("🌙") + self.btn_theme.setObjectName("IconChip") + self.btn_theme.clicked.connect(self.toggle_theme) + + top.addWidget(self.lbl_time) + top.addSpacing(10) + top.addWidget(chip_bt) + top.addWidget(chip_wifi) + top.addWidget(chip_lte) + top.addStretch(1) + top.addWidget(self.lbl_vol) + top.addWidget(self.btn_theme) + + # Content stack + self.stack = QStackedWidget() + self.stack.addWidget(HomeScreen()) # 0 + self.stack.addWidget(StubScreen("Media")) # 1 + self.stack.addWidget(StubScreen("Car")) # 2 + self.stack.addWidget(StubScreen("Maps")) # 3 + self.stack.addWidget(StubScreen("Settings")) # 4 + + # Bottom bar (nav) + self.bottombar = QWidget() + self.bottombar.setObjectName("BottomBar") + bottom = QHBoxLayout(self.bottombar) + bottom.setContentsMargins(16, 8, 16, 8) + bottom.setSpacing(10) + + self.nav_buttons = [ + NavButton("HOME"), + NavButton("MEDIA"), + NavButton("CAR"), + NavButton("MAPS"), + NavButton("SETTINGS"), + ] + + for i, btn in enumerate(self.nav_buttons): + btn.clicked.connect(lambda checked=False, idx=i: self.go(idx)) + bottom.addWidget(btn, 1) + + outer.addWidget(self.topbar) + outer.addWidget(divider(1)) + outer.addWidget(self.stack, 1) + outer.addWidget(divider(1)) + outer.addWidget(self.bottombar) + + self.setCentralWidget(central) + + # timers + self._clock_timer = QTimer(self) + self._clock_timer.timeout.connect(self.update_time) + self._clock_timer.start(500) + + self.apply_theme() + self.go(0) + + def update_time(self): + from datetime import datetime + self.lbl_time.setText(datetime.now().strftime("%H:%M")) + + def apply_theme(self): + self.app.setStyleSheet(THEME_NIGHT if self.is_night else THEME_DAY) + self.btn_theme.setText("🌙" if self.is_night else "☀") + + def toggle_theme(self): + self.is_night = not self.is_night + self.apply_theme() + + def go(self, idx: int): + self.stack.setCurrentIndex(idx) + for i, b in enumerate(self.nav_buttons): + b.set_active(i == idx) diff --git a/ui/top_bar.py b/ui/top_bar.py new file mode 100644 index 0000000..e69de29