Compare commits

..

No commits in common. "4ff5eea2996e937663ab491a75eaa35f7e8e232e" and "6a9bfd4faf8e819431a31d4d60482dd288424152" have entirely different histories.

22 changed files with 91 additions and 810 deletions

2
.gitignore vendored
View File

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

View File

@ -1,42 +0,0 @@
# 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,22 +1,16 @@
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())
@ -28,50 +22,7 @@ def _apply_startup_sound_defaults():
premute_volume = build_info.DEFAULT_PREMUTE_VOLUME
ducking_volume = build_info.DEFAULT_DUCKING_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())
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)

0
build_app.sh Executable file → Normal 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,7 +12,6 @@ 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 Executable file → Normal file
View File

View File

@ -5,125 +5,79 @@ from PySide6.QtWidgets import (
QHBoxLayout,
QScrollArea,
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QGuiApplication
from PySide6.QtWidgets import QScroller
import build_info
class AboutScreen(QWidget):
dev_unlocked = Signal()
def build_about_screen() -> QWidget:
screen = QWidget()
layout = QVBoxLayout(screen)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
def __init__(self):
super().__init__()
self._dev_taps = 0
self._build_row: _InfoRow | None = None
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.dev_is_unlocked = build_info.DEV_MODE_ENABLE
scroller = QScroller.scroller(scroll.viewport())
scroller.grabGesture(
scroll.viewport(),
QScroller.LeftMouseButtonGesture
)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
content = QWidget()
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(12)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
info = QVBoxLayout()
info.setContentsMargins(0, 0, 0, 0)
info.setSpacing(8)
scroller = QScroller.scroller(scroll.viewport())
scroller.grabGesture(
scroll.viewport(),
QScroller.LeftMouseButtonGesture
)
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()))
content = QWidget()
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(12)
content_layout.addLayout(info)
content_layout.addStretch(1)
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
scroll.setWidget(content)
layout.addWidget(scroll)
return screen
class _InfoRow(QWidget):
clicked = Signal()
def _info_row(label: str, value: str) -> QWidget:
row = QWidget()
layout = QHBoxLayout(row)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(12)
def __init__(self, label: str, value: str, clickable: bool = False):
super().__init__()
self._base_label = label
if clickable:
self.setCursor(Qt.PointingHandCursor)
lbl = QLabel(label)
lbl.setFont(QFont("", 15, 600))
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 6, 12, 6)
layout.setSpacing(12)
val = QLabel(value)
val.setFont(QFont("", 15))
val.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
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)
layout.addWidget(lbl)
layout.addStretch(1)
layout.addWidget(val)
return row
def _get_runtime_resolution() -> str:

View File

@ -1,12 +1,4 @@
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
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton
def build_dev_screen(on_exit) -> QWidget:
@ -15,8 +7,6 @@ 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)
@ -24,77 +14,9 @@ def build_dev_screen(on_exit) -> QWidget:
exit_btn = QPushButton("Переход к рабочему столу")
exit_btn.setObjectName("DevExitBtn")
exit_btn.setMinimumHeight(72)
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)
exit_btn.clicked.connect(on_exit)
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

@ -1,249 +0,0 @@
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,12 +13,10 @@ 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 AboutScreen
from screens.setting.about_screen import build_about_screen
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):
@ -62,12 +60,9 @@ 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)
@ -120,7 +115,6 @@ class SettingsScreen(QWidget):
("Звук", "Громкость, эквалайзер"),
],
)
display_row = display_rows.get("Экран")
sound_row = display_rows.get("Звук")
system_rows = self._add_section(
@ -132,39 +126,30 @@ class SettingsScreen(QWidget):
],
)
about_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)
dev_row = system_rows.get("Параметры разработчика")
content_layout.addStretch(1)
scroll.setWidget(content)
list_layout.addWidget(scroll, 1)
self._dev_screen = build_dev_screen(self._exit_app)
self._about_screen = AboutScreen()
self._about_screen.dev_unlocked.connect(self._enable_dev_mode)
self._about_screen = build_about_screen()
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)
if self._dev_enabled:
self.stack.addWidget(self._dev_screen)
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 self._dev_row is not None and self._dev_enabled:
self._dev_row.clicked.connect(self._show_dev)
if dev_row is not None:
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()
@ -211,10 +196,6 @@ 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)
@ -223,12 +204,3 @@ 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 Executable file → Normal file
View File

20
styles/style.qss Normal file
View File

@ -0,0 +1,20 @@
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,51 +167,4 @@ 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,49 +154,4 @@ 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; }
"""

View File

@ -1,68 +0,0 @@
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))

View File

@ -1,77 +0,0 @@
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, QSettings
from PySide6.QtCore import QSize, Qt, QTimer
from PySide6.QtGui import QFont
from themes import THEME_DAY, THEME_NIGHT
from screens.media import MediaScreen
@ -22,9 +22,7 @@ class MainWindowNew(QMainWindow):
def __init__(self, app: QApplication):
super().__init__()
self.app = app
self._settings = QSettings("car_ui", "ui")
theme_pref = self._settings.value("display/theme", "night")
self.is_night = theme_pref != "day"
self.is_night = True
self.setWindowTitle("Car UI (New)")
self.setMinimumSize(QSize(1024, 600))
@ -102,7 +100,6 @@ 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)
@ -121,10 +118,6 @@ 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)