Compare commits
7 Commits
6a9bfd4faf
...
4ff5eea299
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ff5eea299 | ||
|
|
cc0179ca6c | ||
|
|
5f72775d1a | ||
|
|
5c9dc557dd | ||
|
|
60e42e44aa | ||
|
|
d68451230f | ||
|
|
f2e5126fb7 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
42
README.md
Normal 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
57
app.py
@ -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
0
build_app.sh
Normal file → Executable 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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
249
screens/setting/display_screen.py
Normal file
249
screens/setting/display_screen.py
Normal 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
|
||||
@ -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
0
setup_venv.sh
Normal file → Executable 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;
|
||||
}
|
||||
@ -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; }
|
||||
"""
|
||||
|
||||
@ -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
68
ui/confirm_dialog.py
Normal 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
77
ui/language_dialog.py
Normal 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"
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user