diff --git a/app.py b/app.py index 76de432..d430098 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,5 @@ import sys +from pathlib import Path from PySide6.QtCore import QSettings from PySide6.QtWidgets import QApplication @@ -10,7 +11,9 @@ from ui.main_window_new import MainWindowNew def run_app(): app = QApplication(sys.argv) + _apply_reset_if_requested() _apply_startup_sound_defaults() + _apply_startup_display_defaults() window = MainWindowNew(app) window.show() sys.exit(app.exec()) @@ -22,7 +25,39 @@ 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() diff --git a/screens/setting/display_screen.py b/screens/setting/display_screen.py new file mode 100644 index 0000000..a27c209 --- /dev/null +++ b/screens/setting/display_screen.py @@ -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 diff --git a/screens/settings.py b/screens/settings.py index f35c23d..27dceae 100644 --- a/screens/settings.py +++ b/screens/settings.py @@ -17,6 +17,7 @@ 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 class SettingsRow(QPushButton): @@ -60,6 +61,7 @@ class SettingsRow(QPushButton): class SettingsScreen(QWidget): view_changed = Signal(str, bool) + theme_changed = Signal(str) def __init__(self): super().__init__() @@ -115,6 +117,7 @@ class SettingsScreen(QWidget): ("Звук", "Громкость, эквалайзер"), ], ) + display_row = display_rows.get("Экран") sound_row = display_rows.get("Звук") system_rows = self._add_section( @@ -137,12 +140,15 @@ class SettingsScreen(QWidget): 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) 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) @@ -150,6 +156,8 @@ class SettingsScreen(QWidget): 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 +204,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) diff --git a/ui/main_window_new.py b/ui/main_window_new.py index 2b8a9c4..42483fb 100644 --- a/ui/main_window_new.py +++ b/ui/main_window_new.py @@ -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)