Compare commits

...

7 Commits

Author SHA1 Message Date
Your Name
4ff5eea299 add readme 2026-01-09 04:18:00 +03:00
Your Name
cc0179ca6c add hard reboot 2026-01-09 03:59:59 +03:00
Your Name
5f72775d1a add enable dev mode info 2026-01-09 03:53:12 +03:00
Your Name
5c9dc557dd add enable dev mode info 2026-01-09 03:48:15 +03:00
Your Name
60e42e44aa add enable dev mode info 2026-01-09 03:40:15 +03:00
Your Name
d68451230f add language info 2026-01-09 03:27:48 +03:00
Your Name
f2e5126fb7 add display info 2026-01-09 03:21:24 +03:00
22 changed files with 810 additions and 91 deletions

2
.gitignore vendored
View File

@ -6,6 +6,8 @@ config/SSL/privkey.pem
logs/
SECRET_KEY.key
car_ui.spec
dev_mode_enable
reset
repository/achievement_repository.py
service/achievement_service.py

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# car_ui
Интерфейс магнитолы для Raspberry Pi (PySide6), экран 1024x600.
## Требования
- Python 3.13.5
## Как запускать
Право на запуск:
```bash
chmod +x setup_venv.sh run_ui.sh build_app.sh
```
Запуск:
```bash
./setup_venv.sh
./run_ui.sh
./build_app.sh
```
## Скрипты
- `setup_venv.sh` — создает виртуальное окружение `.venv` и устанавливает зависимости из `requirements.txt`.
- `run_ui.sh` — запускает UI локально, выставляя `DISPLAY=:0` и выполняя `main.py`.
- `build_app.sh` — собирает приложение через PyInstaller в `dist/` (имя `car_ui`).
## Конфиг
Файл конфигурации UI находится здесь: `~/.config/car_ui/ui.conf`.
## Файлы-маркеры в корне
В корне проекта могут находиться специальные файлы-маркеры:
- `reset` — сигнал сброса до заводских настроек.
- `dev_mode_enable` — сигнал включения режима разработчика.
Если нужен соответствующий режим, создайте пустой файл с нужным именем; удалите файл, чтобы отключить режим.

57
app.py
View File

@ -1,16 +1,22 @@
import sys
from pathlib import Path
from PySide6.QtCore import QSettings
from PySide6.QtWidgets import QApplication
import build_info
from audio.system_volume import set_volume
from themes import THEME_DAY, THEME_NIGHT
from ui.language_dialog import LanguageDialog
from ui.main_window_new import MainWindowNew
def run_app():
app = QApplication(sys.argv)
_apply_reset_if_requested()
_ensure_language_selected(app)
_apply_startup_sound_defaults()
_apply_startup_display_defaults()
window = MainWindowNew(app)
window.show()
sys.exit(app.exec())
@ -22,7 +28,50 @@ def _apply_startup_sound_defaults():
premute_volume = build_info.DEFAULT_PREMUTE_VOLUME
ducking_volume = build_info.DEFAULT_DUCKING_VOLUME
settings.setValue("sound/base_volume", base_volume)
settings.setValue("sound/premute_volume", premute_volume)
settings.setValue("sound/ducking_volume", ducking_volume)
set_volume(base_volume)
if not settings.contains("sound/base_volume"):
settings.setValue("sound/base_volume", base_volume)
if not settings.contains("sound/premute_volume"):
settings.setValue("sound/premute_volume", premute_volume)
if not settings.contains("sound/ducking_volume"):
settings.setValue("sound/ducking_volume", ducking_volume)
applied_volume = settings.value("sound/base_volume", base_volume)
try:
applied_volume = int(applied_volume)
except (TypeError, ValueError):
applied_volume = base_volume
set_volume(applied_volume)
def _apply_startup_display_defaults():
settings = QSettings("car_ui", "ui")
if not settings.contains("display/brightness"):
settings.setValue("display/brightness", 70)
if not settings.contains("display/auto_brightness"):
settings.setValue("display/auto_brightness", False)
if not settings.contains("display/sleep_minutes"):
settings.setValue("display/sleep_minutes", 10)
if not settings.contains("display/theme"):
settings.setValue("display/theme", "night")
def _apply_reset_if_requested():
reset_marker = Path(__file__).resolve().parent / "reset"
if not reset_marker.exists():
return
settings = QSettings("car_ui", "ui")
settings_path = Path(settings.fileName())
if settings_path.exists():
settings_path.unlink()
reset_marker.unlink()
def _ensure_language_selected(app: QApplication):
settings = QSettings("car_ui", "ui")
if settings.contains("ui/language"):
return
theme_key = settings.value("display/theme", "night")
app.setStyleSheet(THEME_NIGHT if theme_key != "day" else THEME_DAY)
dialog = LanguageDialog()
if dialog.exec() == LanguageDialog.Accepted:
settings.setValue("ui/language", dialog.selected_language())

0
build_app.sh Normal file → Executable file
View File

View File

@ -1,7 +1,7 @@
import os
import platform
import shutil
import socket
from pathlib import Path
APP_NAME = "Car UI"
@ -12,6 +12,7 @@ DEVICE_MODEL = "Raspberry Pi"
DEFAULT_SOUND_VOLUME = 100
DEFAULT_PREMUTE_VOLUME = 10
DEFAULT_DUCKING_VOLUME = 35
DEV_MODE_ENABLE = (Path(__file__).resolve().parent / "dev_mode_enable").exists()
def get_device_model() -> str:

0
run_ui.sh Normal file → Executable file
View File

View File

@ -5,79 +5,125 @@ from PySide6.QtWidgets import (
QHBoxLayout,
QScrollArea,
)
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QGuiApplication
from PySide6.QtWidgets import QScroller
import build_info
def build_about_screen() -> QWidget:
screen = QWidget()
layout = QVBoxLayout(screen)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
class AboutScreen(QWidget):
dev_unlocked = Signal()
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def __init__(self):
super().__init__()
self._dev_taps = 0
self._build_row: _InfoRow | None = None
scroller = QScroller.scroller(scroll.viewport())
scroller.grabGesture(
scroll.viewport(),
QScroller.LeftMouseButtonGesture
)
self.dev_is_unlocked = build_info.DEV_MODE_ENABLE
content = QWidget()
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(12)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
info = QVBoxLayout()
info.setContentsMargins(0, 0, 0, 0)
info.setSpacing(8)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
info.addWidget(_info_row("Приложение", build_info.APP_NAME))
info.addWidget(_info_row("Версия", build_info.VERSION))
info.addWidget(_info_row("Сборка", build_info.BUILD_DATE))
info.addWidget(_info_row("Коммит", build_info.GIT_HASH))
info.addWidget(_info_row("Устройство", build_info.get_device_model()))
info.addWidget(_info_row("ОС", build_info.get_os_pretty_name()))
info.addWidget(_info_row("Ядро", build_info.get_kernel_version()))
info.addWidget(_info_row("RAM (используется/всего)", build_info.get_ram_info()))
info.addWidget(_info_row("Диск (используется/всего)", build_info.get_disk_info()))
info.addWidget(_info_row("Экран", build_info.get_display_resolution()))
info.addWidget(_info_row("Экран (факт)", _get_runtime_resolution()))
info.addWidget(_info_row("Серийный номер", build_info.get_serial_number()))
info.addWidget(_info_row("IP", build_info.get_ip_address()))
info.addWidget(_info_row("Температура CPU", build_info.get_cpu_temp()))
scroller = QScroller.scroller(scroll.viewport())
scroller.grabGesture(
scroll.viewport(),
QScroller.LeftMouseButtonGesture
)
content_layout.addLayout(info)
content_layout.addStretch(1)
content = QWidget()
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(12)
scroll.setWidget(content)
layout.addWidget(scroll)
return screen
info = QVBoxLayout()
info.setContentsMargins(0, 0, 0, 0)
info.setSpacing(8)
info.addWidget(_InfoRow("Приложение", build_info.APP_NAME))
info.addWidget(_InfoRow("Версия", build_info.VERSION))
self._build_row = _InfoRow("Сборка", build_info.BUILD_DATE, clickable=True)
self._build_row.clicked.connect(self._on_build_tap)
info.addWidget(self._build_row)
info.addWidget(_InfoRow("Коммит", build_info.GIT_HASH))
info.addWidget(_InfoRow("Устройство", build_info.get_device_model()))
info.addWidget(_InfoRow("ОС", build_info.get_os_pretty_name()))
info.addWidget(_InfoRow("Ядро", build_info.get_kernel_version()))
info.addWidget(_InfoRow("RAM (используется/всего)", build_info.get_ram_info()))
info.addWidget(_InfoRow("Диск (используется/всего)", build_info.get_disk_info()))
info.addWidget(_InfoRow("Экран", build_info.get_display_resolution()))
info.addWidget(_InfoRow("Экран (факт)", _get_runtime_resolution()))
info.addWidget(_InfoRow("Серийный номер", build_info.get_serial_number()))
info.addWidget(_InfoRow("IP", build_info.get_ip_address()))
info.addWidget(_InfoRow("Температура CPU", build_info.get_cpu_temp()))
content_layout.addLayout(info)
content_layout.addStretch(1)
scroll.setWidget(content)
layout.addWidget(scroll)
def _on_build_tap(self):
if self.dev_is_unlocked:
return
self._dev_taps += 1
remaining = 5 - self._dev_taps
if self._build_row is None:
return
if remaining > 0:
self._build_row.set_suffix(f"(Осталось {remaining} нажат.)")
return
if self._dev_taps >= 5:
#self._dev_taps = 0
self._build_row.set_suffix("(Режим разработчика включен)")
self.dev_unlocked.emit()
self.dev_is_unlocked = True
def _info_row(label: str, value: str) -> QWidget:
row = QWidget()
layout = QHBoxLayout(row)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(12)
class _InfoRow(QWidget):
clicked = Signal()
lbl = QLabel(label)
lbl.setFont(QFont("", 15, 600))
def __init__(self, label: str, value: str, clickable: bool = False):
super().__init__()
self._base_label = label
if clickable:
self.setCursor(Qt.PointingHandCursor)
val = QLabel(value)
val.setFont(QFont("", 15))
val.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(12)
layout.addWidget(lbl)
layout.addStretch(1)
layout.addWidget(val)
return row
self._label = QLabel(label)
self._label.setFont(QFont("", 15, 600))
self._value = QLabel(value)
self._value.setFont(QFont("", 15))
self._value.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.addWidget(self._label)
layout.addStretch(1)
layout.addWidget(self._value)
self._clickable = clickable
def mousePressEvent(self, event):
if self._clickable and event.button() == Qt.LeftButton:
self.clicked.emit()
super().mousePressEvent(event)
def set_suffix(self, suffix: str):
suffix = suffix.strip()
if suffix:
self._label.setText(f"{self._base_label} {suffix}")
else:
self._label.setText(self._base_label)
def _get_runtime_resolution() -> str:

View File

@ -1,4 +1,12 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton
from pathlib import Path
import subprocess
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
import build_info
from ui.confirm_dialog import ConfirmDialog
def build_dev_screen(on_exit) -> QWidget:
@ -7,6 +15,8 @@ def build_dev_screen(on_exit) -> QWidget:
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
layout.addWidget(_build_persist_toggle())
hdr = QHBoxLayout()
hdr.setContentsMargins(0, 0, 0, 0)
hdr.setSpacing(12)
@ -14,9 +24,77 @@ def build_dev_screen(on_exit) -> QWidget:
exit_btn = QPushButton("Переход к рабочему столу")
exit_btn.setObjectName("DevExitBtn")
exit_btn.setMinimumHeight(72)
exit_btn.clicked.connect(on_exit)
exit_btn.clicked.connect(lambda: _confirm_exit(on_exit))
reboot_btn = QPushButton("Выполнить перезагрузку")
reboot_btn.setObjectName("DevExitBtn")
reboot_btn.setMinimumHeight(72)
reboot_btn.clicked.connect(_confirm_reboot)
layout.addLayout(hdr)
layout.addWidget(exit_btn)
layout.addWidget(reboot_btn)
layout.addStretch(1)
return screen
def _build_persist_toggle() -> QWidget:
row = QWidget()
layout = QHBoxLayout(row)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(12)
lbl = QLabel("Показывать после перезагрузки")
lbl.setFont(QFont("", 14, 600))
btn = QPushButton("Выкл")
btn.setObjectName("SoundToggle")
btn.setCheckable(True)
btn.setChecked(_dev_flag_path().exists())
btn.setMinimumHeight(40)
btn.setMinimumWidth(110)
btn.setFont(QFont("", 12, 700))
def _sync_text(is_checked: bool):
btn.setText("Вкл" if is_checked else "Выкл")
def _persist_flag(is_checked: bool):
flag_path = _dev_flag_path()
if is_checked:
flag_path.touch(exist_ok=True)
else:
if flag_path.exists():
flag_path.unlink()
btn.toggled.connect(_sync_text)
btn.toggled.connect(_persist_flag)
_sync_text(btn.isChecked())
layout.addWidget(lbl)
layout.addStretch(1)
layout.addWidget(btn)
return row
def _dev_flag_path() -> Path:
return Path(build_info.__file__).resolve().parent / "dev_mode_enable"
def _confirm_exit(on_exit):
dialog = ConfirmDialog(
"Подтверждение",
"Закрыть приложение и перейти к рабочему столу?",
"Выйти",
)
if dialog.exec() == ConfirmDialog.Accepted:
on_exit()
def _confirm_reboot():
dialog = ConfirmDialog(
"Подтверждение",
"Выполнить перезагрузку устройства?",
"Перезагрузить",
)
if dialog.exec() == ConfirmDialog.Accepted:
subprocess.run(["sudo", "reboot"], check=False)

View File

@ -0,0 +1,249 @@
from PySide6.QtCore import Qt, QSettings, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QSlider,
QButtonGroup,
QScrollArea,
QScroller,
)
class DisplayScreen(QWidget):
theme_changed = Signal(str)
def __init__(self):
super().__init__()
self._settings = QSettings("car_ui", "ui")
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(12)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroller = QScroller.scroller(scroll.viewport())
scroller.grabGesture(
scroll.viewport(),
QScroller.LeftMouseButtonGesture,
)
content = QWidget()
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(12)
content_layout.addWidget(self._build_brightness_card())
content_layout.addWidget(self._build_sleep_card())
content_layout.addWidget(self._build_theme_card())
content_layout.addStretch(1)
scroll.setWidget(content)
root.addWidget(scroll, 1)
def _build_brightness_card(self) -> QWidget:
card, body = _card("Яркость")
brightness = _read_int_setting(self._settings, "display/brightness", 70)
auto_brightness = _read_bool_setting(
self._settings,
"display/auto_brightness",
False,
)
row_toggle, toggle_btn = _toggle_row(
"Автояркость",
checked=auto_brightness,
)
row_slider, slider, value_label = _slider_row(
"Яркость экрана",
10,
100,
brightness,
lambda v: f"{v}%",
)
slider.setEnabled(not auto_brightness)
toggle_btn.toggled.connect(
lambda v: self._settings.setValue("display/auto_brightness", v)
)
toggle_btn.toggled.connect(lambda v: slider.setEnabled(not v))
slider.valueChanged.connect(
lambda v: self._settings.setValue("display/brightness", v)
)
body.addWidget(row_toggle)
body.addWidget(row_slider)
return card
def _build_sleep_card(self) -> QWidget:
card, body = _card("Сон")
sleep_minutes = _read_int_setting(
self._settings,
"display/sleep_minutes",
10,
)
row, slider, value_label = _slider_row(
"Отключать экран через",
0,
30,
sleep_minutes,
_format_sleep_minutes,
)
slider.valueChanged.connect(
lambda v: self._settings.setValue("display/sleep_minutes", v)
)
body.addWidget(row)
return card
def _build_theme_card(self) -> QWidget:
card, body = _card("Тема")
row = QWidget()
row.setObjectName("SoundToneRow")
layout = QHBoxLayout(row)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
group = QButtonGroup(row)
group.setExclusive(True)
current_theme = self._settings.value("display/theme", "night")
theme_map = {}
for label, key in (("День", "day"), ("Ночь", "night")):
btn = QPushButton(label)
btn.setObjectName("SoundToneBtn")
btn.setCheckable(True)
btn.setMinimumHeight(40)
btn.setFont(QFont("", 13, 600))
if key == current_theme:
btn.setChecked(True)
group.addButton(btn)
theme_map[btn] = key
layout.addWidget(btn, 1)
def _apply_theme(btn: QPushButton):
theme_key = theme_map.get(btn, "night")
self._settings.setValue("display/theme", theme_key)
self.theme_changed.emit(theme_key)
group.buttonClicked.connect(_apply_theme)
body.addWidget(row)
return card
def _card(title: str) -> tuple[QWidget, QVBoxLayout]:
card = QWidget()
card.setObjectName("SoundCard")
layout = QVBoxLayout(card)
layout.setContentsMargins(14, 12, 14, 12)
layout.setSpacing(10)
header = QLabel(title)
header.setObjectName("SoundCardTitle")
header.setFont(QFont("", 14, 700))
layout.addWidget(header)
return card, layout
def _toggle_row(label: str, checked: bool) -> tuple[QWidget, QPushButton]:
row = QWidget()
row.setObjectName("SoundToggleRow")
layout = QHBoxLayout(row)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
lbl = QLabel(label)
lbl.setFont(QFont("", 13, 600))
btn = QPushButton("Выкл")
btn.setObjectName("SoundToggle")
btn.setCheckable(True)
btn.setChecked(checked)
btn.setMinimumHeight(36)
btn.setMinimumWidth(86)
btn.setFont(QFont("", 12, 700))
def _sync_text(is_checked: bool):
btn.setText("Вкл" if is_checked else "Выкл")
btn.toggled.connect(_sync_text)
_sync_text(btn.isChecked())
layout.addWidget(lbl)
layout.addStretch(1)
layout.addWidget(btn)
return row, btn
def _slider_row(
label: str,
minimum: int,
maximum: int,
value: int,
formatter,
) -> tuple[QWidget, QSlider, QLabel]:
row = QWidget()
row.setObjectName("SoundSliderRow")
layout = QVBoxLayout(row)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(6)
header = QWidget()
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(8)
lbl = QLabel(label)
lbl.setFont(QFont("", 13, 600))
val = QLabel(formatter(value))
val.setObjectName("SoundValue")
val.setFont(QFont("", 12, 600))
val.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
header_layout.addWidget(lbl)
header_layout.addStretch(1)
header_layout.addWidget(val)
slider = QSlider(Qt.Horizontal)
slider.setObjectName("SoundSlider")
slider.setRange(minimum, maximum)
slider.setValue(value)
slider.valueChanged.connect(lambda v: val.setText(formatter(v)))
layout.addWidget(header)
layout.addWidget(slider)
return row, slider, val
def _format_sleep_minutes(value: int) -> str:
if value <= 0:
return "Никогда"
return f"{value} мин"
def _read_int_setting(settings: QSettings, key: str, default: int) -> int:
raw = settings.value(key, default)
try:
return int(raw)
except (TypeError, ValueError):
return default
def _read_bool_setting(settings: QSettings, key: str, default: bool) -> bool:
raw = settings.value(key, default)
if isinstance(raw, bool):
return raw
if isinstance(raw, (int, float)):
return bool(raw)
if isinstance(raw, str):
return raw.strip().lower() in {"1", "true", "yes", "on"}
return default

View File

@ -13,10 +13,12 @@ from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QScroller
from screens.setting.bluetooth_screen import BluetoothScreen
from screens.setting.about_screen import build_about_screen
from screens.setting.about_screen import AboutScreen
from screens.setting.dev_screen import build_dev_screen
from screens.setting.sound_screen import SoundScreen
from screens.setting.eq_screen import EqualizerScreen
from screens.setting.display_screen import DisplayScreen
import build_info
class SettingsRow(QPushButton):
@ -60,9 +62,12 @@ class SettingsRow(QPushButton):
class SettingsScreen(QWidget):
view_changed = Signal(str, bool)
theme_changed = Signal(str)
def __init__(self):
super().__init__()
self._dev_enabled = build_info.DEV_MODE_ENABLE
self._dev_row: SettingsRow | None = None
root = QVBoxLayout(self)
root.setContentsMargins(18, 16, 18, 16)
root.setSpacing(12)
@ -115,6 +120,7 @@ class SettingsScreen(QWidget):
("Звук", "Громкость, эквалайзер"),
],
)
display_row = display_rows.get("Экран")
sound_row = display_rows.get("Звук")
system_rows = self._add_section(
@ -126,30 +132,39 @@ class SettingsScreen(QWidget):
],
)
about_row = system_rows.get("Об устройстве")
dev_row = system_rows.get("Параметры разработчика")
self._dev_row = system_rows.get("Параметры разработчика")
if self._dev_row is not None and not self._dev_enabled:
self._dev_row.setVisible(False)
content_layout.addStretch(1)
scroll.setWidget(content)
list_layout.addWidget(scroll, 1)
self._dev_screen = build_dev_screen(self._exit_app)
self._about_screen = build_about_screen()
self._about_screen = AboutScreen()
self._about_screen.dev_unlocked.connect(self._enable_dev_mode)
self._bt_screen = BluetoothScreen(self._show_list)
self._eq_screen = EqualizerScreen()
self._sound_screen = SoundScreen(self._show_equalizer)
self._display_screen = DisplayScreen()
self._display_screen.theme_changed.connect(self.theme_changed.emit)
self.stack.addWidget(self._list_screen)
self.stack.addWidget(self._dev_screen)
if self._dev_enabled:
self.stack.addWidget(self._dev_screen)
self.stack.addWidget(self._about_screen)
self.stack.addWidget(self._bt_screen)
self.stack.addWidget(self._sound_screen)
self.stack.addWidget(self._eq_screen)
self.stack.addWidget(self._display_screen)
if dev_row is not None:
dev_row.clicked.connect(self._show_dev)
if self._dev_row is not None and self._dev_enabled:
self._dev_row.clicked.connect(self._show_dev)
if about_row is not None:
about_row.clicked.connect(self._show_about)
if bt_row is not None:
bt_row.clicked.connect(self._show_bluetooth)
if display_row is not None:
display_row.clicked.connect(self._show_display)
if sound_row is not None:
sound_row.clicked.connect(self._show_sound)
self._show_list()
@ -196,6 +211,10 @@ class SettingsScreen(QWidget):
self.stack.setCurrentWidget(self._sound_screen)
self.view_changed.emit("Звук", True)
def _show_display(self):
self.stack.setCurrentWidget(self._display_screen)
self.view_changed.emit("Экран", True)
def _show_equalizer(self):
self.stack.setCurrentWidget(self._eq_screen)
self.view_changed.emit("Эквалайзер", True)
@ -204,3 +223,12 @@ class SettingsScreen(QWidget):
app = QApplication.instance()
if app is not None:
app.quit()
def _enable_dev_mode(self):
if self._dev_enabled:
return
self._dev_enabled = True
if self._dev_row is not None:
self._dev_row.setVisible(True)
self._dev_row.clicked.connect(self._show_dev)
self.stack.addWidget(self._dev_screen)

0
setup_venv.sh Normal file → Executable file
View File

View File

@ -1,20 +0,0 @@
QWidget {
background-color: #0b0b0b;
}
QPushButton {
background-color: #1a1a1a;
color: white;
font-size: 28px;
border-radius: 16px;
}
QPushButton:pressed {
background-color: #1e90ff;
}
QPushButton#SettingsRow QLabel#SettingsRowTitle,
QPushButton#SettingsRow QLabel#SettingsRowSub,
QPushButton#SettingsRow QLabel#SettingsChevron {
background: transparent;
}

View File

@ -167,4 +167,51 @@ QMenu::item:selected { background: #F3F4F6; }
font-size: 14px;
font-weight: 600;
}
#LanguageDialog { background: #F4F6F8; }
#LanguageCard {
background: #FFFFFF;
border-radius: 16px;
border: 1px solid #E5E7EB;
}
#LanguageTitle { color: #111827; }
#LanguageOption {
background: #FFFFFF;
color: #111827;
border-radius: 12px;
border: 1px solid #E5E7EB;
padding: 8px 14px;
}
#LanguageOption:checked { background: #E5E7EB; }
#LanguageConfirm {
background: #111827;
color: #FFFFFF;
border-radius: 12px;
padding: 10px 16px;
}
#LanguageConfirm:hover { background: #0B1220; }
#ConfirmDialog { background: #F4F6F8; }
#ConfirmCard {
background: #FFFFFF;
border-radius: 16px;
border: 1px solid #E5E7EB;
}
#ConfirmTitle { color: #111827; }
#ConfirmMessage { color: rgba(107,114,128,0.95); }
#ConfirmCancel {
background: #FFFFFF;
color: #111827;
border-radius: 12px;
border: 1px solid #E5E7EB;
padding: 8px 14px;
}
#ConfirmCancel:hover { background: #F9FAFB; }
#ConfirmOk {
background: #111827;
color: #FFFFFF;
border-radius: 12px;
padding: 8px 14px;
}
#ConfirmOk:hover { background: #0B1220; }
"""

View File

@ -154,4 +154,49 @@ QMenu::item:selected { background: #1B2330; }
font-size: 14px;
font-weight: 600;
}
#LanguageDialog { background: #0B0E11; }
#LanguageCard {
background: #141A22;
border-radius: 16px;
}
#LanguageTitle { color: #E6EAF0; }
#LanguageOption {
background: #141A22;
color: #E6EAF0;
border-radius: 12px;
border: 1px solid #1B2330;
padding: 8px 14px;
}
#LanguageOption:checked { background: #2A3A52; }
#LanguageConfirm {
background: #2A3A52;
color: #E6EAF0;
border-radius: 12px;
padding: 10px 16px;
}
#LanguageConfirm:hover { background: #344968; }
#ConfirmDialog { background: #0B0E11; }
#ConfirmCard {
background: #141A22;
border-radius: 16px;
}
#ConfirmTitle { color: #E6EAF0; }
#ConfirmMessage { color: rgba(138,147,166,0.95); }
#ConfirmCancel {
background: #141A22;
color: #E6EAF0;
border-radius: 12px;
border: 1px solid #1B2330;
padding: 8px 14px;
}
#ConfirmCancel:hover { background: #1B2330; }
#ConfirmOk {
background: #2A3A52;
color: #E6EAF0;
border-radius: 12px;
padding: 8px 14px;
}
#ConfirmOk:hover { background: #344968; }
"""

68
ui/confirm_dialog.py Normal file
View File

@ -0,0 +1,68 @@
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QSpacerItem,
QSizePolicy,
)
class ConfirmDialog(QDialog):
def __init__(self, title: str, message: str, confirm_text: str):
super().__init__()
self.setObjectName("ConfirmDialog")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setWindowModality(Qt.ApplicationModal)
self.setMinimumSize(QSize(1024, 600))
root = QVBoxLayout(self)
root.setContentsMargins(24, 24, 24, 24)
root.setSpacing(16)
root.addItem(QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding))
card = QWidget()
card.setObjectName("ConfirmCard")
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(18, 18, 18, 18)
card_layout.setSpacing(12)
title_lbl = QLabel(title)
title_lbl.setObjectName("ConfirmTitle")
title_lbl.setFont(QFont("", 20, 700))
msg_lbl = QLabel(message)
msg_lbl.setObjectName("ConfirmMessage")
msg_lbl.setFont(QFont("", 14, 500))
msg_lbl.setWordWrap(True)
actions = QHBoxLayout()
actions.setContentsMargins(0, 0, 0, 0)
actions.setSpacing(12)
cancel_btn = QPushButton("Отмена")
cancel_btn.setObjectName("ConfirmCancel")
cancel_btn.setMinimumHeight(50)
cancel_btn.setFont(QFont("", 14, 700))
cancel_btn.clicked.connect(self.reject)
ok_btn = QPushButton(confirm_text)
ok_btn.setObjectName("ConfirmOk")
ok_btn.setMinimumHeight(50)
ok_btn.setFont(QFont("", 14, 700))
ok_btn.clicked.connect(self.accept)
actions.addWidget(cancel_btn, 1)
actions.addWidget(ok_btn, 1)
card_layout.addWidget(title_lbl)
card_layout.addWidget(msg_lbl)
card_layout.addLayout(actions)
root.addWidget(card)
root.addItem(QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding))

77
ui/language_dialog.py Normal file
View File

@ -0,0 +1,77 @@
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QButtonGroup,
QSpacerItem,
QSizePolicy,
)
class LanguageDialog(QDialog):
def __init__(self):
super().__init__()
self.setObjectName("LanguageDialog")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setWindowModality(Qt.ApplicationModal)
self.setMinimumSize(QSize(1024, 600))
root = QVBoxLayout(self)
root.setContentsMargins(24, 24, 24, 24)
root.setSpacing(16)
root.addItem(QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding))
card = QWidget()
card.setObjectName("LanguageCard")
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(18, 18, 18, 18)
card_layout.setSpacing(14)
title = QLabel("Сначала выберите язык")
title.setObjectName("LanguageTitle")
title.setFont(QFont("", 20, 700))
title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
options_row = QHBoxLayout()
options_row.setContentsMargins(0, 0, 0, 0)
options_row.setSpacing(12)
self._group = QButtonGroup(self)
self._group.setExclusive(True)
self._btn_ru = QPushButton("Русский")
self._btn_ru.setObjectName("LanguageOption")
self._btn_ru.setCheckable(True)
self._btn_ru.setChecked(True)
self._btn_ru.setMinimumHeight(48)
self._btn_ru.setFont(QFont("", 14, 600))
self._group.addButton(self._btn_ru)
options_row.addWidget(self._btn_ru, 1)
options_row.addStretch(1)
confirm = QPushButton("Продолжить")
confirm.setObjectName("LanguageConfirm")
confirm.setMinimumHeight(52)
confirm.setFont(QFont("", 15, 700))
confirm.clicked.connect(self.accept)
card_layout.addWidget(title)
card_layout.addLayout(options_row)
card_layout.addWidget(confirm)
root.addWidget(card)
root.addItem(QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding))
def reject(self):
# Ignore reject to force a language choice.
return
def selected_language(self) -> str:
return "ru"

View File

@ -10,7 +10,7 @@ from PySide6.QtWidgets import (
QPushButton,
QLabel,
)
from PySide6.QtCore import QSize, Qt, QTimer
from PySide6.QtCore import QSize, Qt, QTimer, QSettings
from PySide6.QtGui import QFont
from themes import THEME_DAY, THEME_NIGHT
from screens.media import MediaScreen
@ -22,7 +22,9 @@ class MainWindowNew(QMainWindow):
def __init__(self, app: QApplication):
super().__init__()
self.app = app
self.is_night = True
self._settings = QSettings("car_ui", "ui")
theme_pref = self._settings.value("display/theme", "night")
self.is_night = theme_pref != "day"
self.setWindowTitle("Car UI (New)")
self.setMinimumSize(QSize(1024, 600))
@ -100,6 +102,7 @@ class MainWindowNew(QMainWindow):
self.act_maps.triggered.connect(lambda: self.go(2))
self.media_screen.source_changed.connect(self.lbl_bt.setText)
self.settings_screen.view_changed.connect(self._on_settings_view_changed)
self.settings_screen.theme_changed.connect(self._on_theme_changed)
self.btn_back.clicked.connect(self._settings_back)
outer.addWidget(self.topbar)
@ -118,6 +121,10 @@ class MainWindowNew(QMainWindow):
def apply_theme(self):
self.app.setStyleSheet(THEME_NIGHT if self.is_night else THEME_DAY)
def _on_theme_changed(self, theme_key: str):
self.is_night = theme_key != "day"
self.apply_theme()
def _sync_topbar(self, idx: int):
is_settings = idx == self.settings_idx
self.menu_button.setVisible(not is_settings)