something-x-dev 1.9.0.dev29__tar.gz → 1.9.0.dev30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/PKG-INFO +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/ROADMAP.md +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/_version.py +2 -2
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/device.py +10 -3
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/theme.py +8 -16
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/profiles.py +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/protocol.py +35 -3
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/tray.py +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/PKG-INFO +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/conftest.py +5 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_notifications.py +94 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_profiles.py +2 -2
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_protocol.py +1 -1
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_theme.py +16 -14
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/CODEOWNERS +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/ci.yml +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/release-dev.yml +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/release.yml +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.gitignore +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/DEVICES.md +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/LICENSE +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/PKGBUILD +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/README.md +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/cliff.toml +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/RELEASING.md +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/docs.html +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/index.html +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/flake.nix +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/__init__.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/application.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/bluetooth.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/__init__.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/com.something.x.omarchy.desktop +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/style.css +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/__init__.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/home.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/splash.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/theme.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/window.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/pyproject.toml +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/setup.cfg +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/SOURCES.txt +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/dependency_links.txt +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/entry_points.txt +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/requires.txt +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/top_level.txt +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/somethingx +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/__init__.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_bluetooth.py +0 -0
- {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_crc.py +0 -0
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
### Features
|
|
31
31
|
- [x] **Device nickname** — rename a paired device in the UI; stored in profiles
|
|
32
32
|
- [x] **Profile import / export** — share `.json` profile files between machines or with other users
|
|
33
|
-
- [
|
|
33
|
+
- [x] **Wear-detect MPRIS actions** — pause media when both buds are removed; resume when reinserted (opt-in)
|
|
34
34
|
- [x] **Notification preferences** — per-event toggles (battery low, connect, disconnect) in settings
|
|
35
35
|
- [x] **Theming** — user-selectable accent color and light/dark mode toggle; theme stored in config
|
|
36
36
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.9.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 9, 0, '
|
|
21
|
+
__version__ = version = '1.9.0.dev30'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 0, 'dev30')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -9,7 +9,7 @@ import gi
|
|
|
9
9
|
gi.require_version("Gtk", "4.0")
|
|
10
10
|
gi.require_version("Pango", "1.0")
|
|
11
11
|
gi.require_version("PangoCairo", "1.0")
|
|
12
|
-
from gi.repository import Gtk, GLib, PangoCairo
|
|
12
|
+
from gi.repository import Gtk, GLib, PangoCairo
|
|
13
13
|
|
|
14
14
|
from ..bluetooth import BluetoothDevice, BluetoothManager
|
|
15
15
|
from ..protocol import NothingDevice, ANCMode, EQ_PRESETS
|
|
@@ -477,12 +477,15 @@ class DevicePage(Gtk.Box):
|
|
|
477
477
|
)
|
|
478
478
|
|
|
479
479
|
self._auto_pause_switch = Gtk.Switch()
|
|
480
|
-
self._auto_pause_switch.set_active(
|
|
480
|
+
self._auto_pause_switch.set_active(
|
|
481
|
+
profiles.get_notify_prefs(self._bt_device.address).get("wear_mpris", False)
|
|
482
|
+
)
|
|
481
483
|
self._auto_pause_switch.set_valign(Gtk.Align.CENTER)
|
|
484
|
+
self._auto_pause_switch.connect("state-set", self._on_auto_pause_toggled)
|
|
482
485
|
settings_group.append(
|
|
483
486
|
_settings_row(
|
|
484
487
|
"Auto-Pause",
|
|
485
|
-
"Pause media
|
|
488
|
+
"Pause/resume media with wear detection",
|
|
486
489
|
self._auto_pause_switch,
|
|
487
490
|
)
|
|
488
491
|
)
|
|
@@ -712,6 +715,10 @@ class DevicePage(Gtk.Box):
|
|
|
712
715
|
if not entry.has_focus():
|
|
713
716
|
self._save_nickname()
|
|
714
717
|
|
|
718
|
+
def _on_auto_pause_toggled(self, _switch, state: bool):
|
|
719
|
+
profiles.set_notify_prefs(self._bt_device.address, {"wear_mpris": state})
|
|
720
|
+
return False
|
|
721
|
+
|
|
715
722
|
def _on_notif_battery_toggled(self, _switch, state: bool):
|
|
716
723
|
profiles.set_notify_prefs(self._bt_device.address, {"battery_low": state})
|
|
717
724
|
return False
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import math
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Callable
|
|
3
3
|
|
|
4
4
|
import gi
|
|
5
5
|
|
|
6
6
|
gi.require_version("Gtk", "4.0")
|
|
7
|
-
from gi.repository import Gdk,
|
|
7
|
+
from gi.repository import Gdk, Gtk
|
|
8
8
|
|
|
9
9
|
from ..theme import ACCENT_PRESETS, BG_PRESETS, FONT_PRESETS, TEXTURES, Theme, hex_to_rgb
|
|
10
10
|
|
|
@@ -345,11 +345,8 @@ class ThemePage(Gtk.Box):
|
|
|
345
345
|
|
|
346
346
|
def _on_accent_color_set(self, btn: Gtk.ColorButton):
|
|
347
347
|
rgba = btn.get_rgba()
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
int(rgba.green * 255),
|
|
351
|
-
int(rgba.blue * 255),
|
|
352
|
-
)
|
|
348
|
+
r, g, b = int(rgba.red * 255), int(rgba.green * 255), int(rgba.blue * 255)
|
|
349
|
+
self._theme.accent = f"#{r:02x}{g:02x}{b:02x}"
|
|
353
350
|
self._update_accent_swatches()
|
|
354
351
|
self._emit()
|
|
355
352
|
|
|
@@ -368,11 +365,8 @@ class ThemePage(Gtk.Box):
|
|
|
368
365
|
|
|
369
366
|
def _on_bg_color_set(self, btn: Gtk.ColorButton):
|
|
370
367
|
rgba = btn.get_rgba()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
int(rgba.green * 255),
|
|
374
|
-
int(rgba.blue * 255),
|
|
375
|
-
)
|
|
368
|
+
r, g, b = int(rgba.red * 255), int(rgba.green * 255), int(rgba.blue * 255)
|
|
369
|
+
self._theme.bg_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
376
370
|
self._update_bg_swatches()
|
|
377
371
|
self._emit()
|
|
378
372
|
|
|
@@ -419,8 +413,6 @@ class ThemePage(Gtk.Box):
|
|
|
419
413
|
self._emit()
|
|
420
414
|
|
|
421
415
|
def _on_reset(self, _btn):
|
|
422
|
-
import dataclasses
|
|
423
|
-
|
|
424
416
|
self._theme = Theme()
|
|
425
417
|
self._reload_controls()
|
|
426
418
|
self._emit()
|
|
@@ -428,11 +420,11 @@ class ThemePage(Gtk.Box):
|
|
|
428
420
|
# ── Internal helpers ──────────────────────────────────────────────────────
|
|
429
421
|
|
|
430
422
|
def _update_accent_swatches(self):
|
|
431
|
-
for sw, (color, _) in zip(self._accent_swatches, ACCENT_PRESETS):
|
|
423
|
+
for sw, (color, _) in zip(self._accent_swatches, ACCENT_PRESETS, strict=False):
|
|
432
424
|
sw.set_active(color.lower() == self._theme.accent.lower())
|
|
433
425
|
|
|
434
426
|
def _update_bg_swatches(self):
|
|
435
|
-
for sw, (color, _) in zip(self._bg_swatches, BG_PRESETS):
|
|
427
|
+
for sw, (color, _) in zip(self._bg_swatches, BG_PRESETS, strict=False):
|
|
436
428
|
sw.set_active(color.lower() == self._theme.bg_color.lower())
|
|
437
429
|
|
|
438
430
|
def _reload_controls(self):
|
|
@@ -5,7 +5,7 @@ _DIR = os.path.expanduser("~/.config/something-x")
|
|
|
5
5
|
_PROFILES_FILE = os.path.join(_DIR, "profiles.json")
|
|
6
6
|
_LAST_DEV_FILE = os.path.join(_DIR, "last_device")
|
|
7
7
|
|
|
8
|
-
_NOTIFY_DEFAULTS: dict = {"battery_low": True, "connect": True, "disconnect": True}
|
|
8
|
+
_NOTIFY_DEFAULTS: dict = {"battery_low": True, "connect": True, "disconnect": True, "wear_mpris": False}
|
|
9
9
|
_ALLOWED_IMPORT_KEYS = frozenset({"anc", "eq", "nickname", "notify"})
|
|
10
10
|
|
|
11
11
|
|
|
@@ -152,6 +152,8 @@ class NothingDevice(GObject.Object):
|
|
|
152
152
|
self._thread: threading.Thread | None = None
|
|
153
153
|
self._low_bat_notified: dict[str, set[int]] = {}
|
|
154
154
|
self._low_bat_seen: set[str] = set()
|
|
155
|
+
self._wear_both_removed: bool = False
|
|
156
|
+
self._wear_paused: bool = False
|
|
155
157
|
|
|
156
158
|
# ── Public API ────────────────────────────────────────────────────────────
|
|
157
159
|
|
|
@@ -545,9 +547,8 @@ class NothingDevice(GObject.Object):
|
|
|
545
547
|
else:
|
|
546
548
|
modes = frozenset([ANCMode.OFF, ANCMode.TRANSPARENCY])
|
|
547
549
|
self.state.supported_anc_modes = modes
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
)
|
|
550
|
+
labels = [ANCMode.LABELS.get(m, m) for m in sorted(modes)]
|
|
551
|
+
_log(f"[protocol] supported ANC modes detected: {labels}")
|
|
551
552
|
changed = True
|
|
552
553
|
|
|
553
554
|
return changed
|
|
@@ -579,6 +580,7 @@ class NothingDevice(GObject.Object):
|
|
|
579
580
|
changed = True
|
|
580
581
|
if changed:
|
|
581
582
|
_log(f"[protocol] wearing L={self.state.left_wearing} R={self.state.right_wearing}")
|
|
583
|
+
self._check_wear_mpris()
|
|
582
584
|
return changed
|
|
583
585
|
|
|
584
586
|
# ── Legacy 0x03 frame handling (status-only fallback) ────────────────────
|
|
@@ -681,6 +683,36 @@ class NothingDevice(GObject.Object):
|
|
|
681
683
|
).start()
|
|
682
684
|
break
|
|
683
685
|
|
|
686
|
+
def _check_wear_mpris(self):
|
|
687
|
+
from . import profiles
|
|
688
|
+
|
|
689
|
+
if not profiles.get_notify_prefs(self.address).get("wear_mpris", False):
|
|
690
|
+
self._wear_both_removed = False
|
|
691
|
+
self._wear_paused = False
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
both_removed = not self.state.left_wearing and not self.state.right_wearing
|
|
695
|
+
|
|
696
|
+
if both_removed and not self._wear_both_removed:
|
|
697
|
+
self._wear_both_removed = True
|
|
698
|
+
self._wear_paused = True
|
|
699
|
+
threading.Thread(
|
|
700
|
+
target=subprocess.run,
|
|
701
|
+
args=(["playerctl", "pause"],),
|
|
702
|
+
kwargs={"capture_output": True},
|
|
703
|
+
daemon=True,
|
|
704
|
+
).start()
|
|
705
|
+
elif not both_removed and self._wear_both_removed:
|
|
706
|
+
self._wear_both_removed = False
|
|
707
|
+
if self._wear_paused:
|
|
708
|
+
self._wear_paused = False
|
|
709
|
+
threading.Thread(
|
|
710
|
+
target=subprocess.run,
|
|
711
|
+
args=(["playerctl", "play"],),
|
|
712
|
+
kwargs={"capture_output": True},
|
|
713
|
+
daemon=True,
|
|
714
|
+
).start()
|
|
715
|
+
|
|
684
716
|
def _poll_earphone_status(self):
|
|
685
717
|
# The firmware only computes a fresh per-bud snapshot when asked; the
|
|
686
718
|
# pushed EVT frames carry stale placeholder entries for the bud that
|
|
@@ -4,7 +4,7 @@ import dbus.service
|
|
|
4
4
|
import dbus.mainloop.glib
|
|
5
5
|
from gi.repository import GLib, GObject
|
|
6
6
|
|
|
7
|
-
from .bluetooth import BluetoothManager,
|
|
7
|
+
from .bluetooth import BluetoothManager, device_icon_name
|
|
8
8
|
|
|
9
9
|
_ITEM_IFACE = "org.kde.StatusNotifierItem"
|
|
10
10
|
_WATCHER_IFACE = "org.kde.StatusNotifierWatcher"
|
|
@@ -22,7 +22,11 @@ def mock_profiles(monkeypatch):
|
|
|
22
22
|
monkeypatch.setattr(profiles, "save", MagicMock())
|
|
23
23
|
monkeypatch.setattr(profiles, "load", MagicMock(return_value={}))
|
|
24
24
|
monkeypatch.setattr(profiles, "set_last_device", MagicMock())
|
|
25
|
-
monkeypatch.setattr(
|
|
25
|
+
monkeypatch.setattr(
|
|
26
|
+
profiles,
|
|
27
|
+
"get_notify_prefs",
|
|
28
|
+
MagicMock(return_value={"battery_low": True, "connect": True, "disconnect": True}),
|
|
29
|
+
)
|
|
26
30
|
monkeypatch.setattr(profiles, "get_nickname", MagicMock(return_value=None))
|
|
27
31
|
|
|
28
32
|
|
|
@@ -131,3 +131,97 @@ def test_battery_low_sent_with_default_prefs():
|
|
|
131
131
|
fired.wait(timeout=2)
|
|
132
132
|
|
|
133
133
|
assert calls
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── wear_mpris pref ───────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _set_wearing(dev: NothingDevice, left: bool, right: bool):
|
|
140
|
+
"""Directly update wearing state and call the MPRIS check."""
|
|
141
|
+
dev.state.left_wearing = left
|
|
142
|
+
dev.state.right_wearing = right
|
|
143
|
+
dev._check_wear_mpris()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_wear_mpris_pauses_when_both_removed(tmp_path, monkeypatch):
|
|
147
|
+
monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
|
|
148
|
+
profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
|
|
149
|
+
|
|
150
|
+
calls = []
|
|
151
|
+
|
|
152
|
+
def fake_run(cmd, **kw):
|
|
153
|
+
calls.append(cmd)
|
|
154
|
+
|
|
155
|
+
dev = _make_device()
|
|
156
|
+
with patch("nothing_app.protocol.subprocess.run", fake_run):
|
|
157
|
+
_set_wearing(dev, True, True) # both wearing
|
|
158
|
+
_set_wearing(dev, False, False) # both removed → pause
|
|
159
|
+
import time
|
|
160
|
+
|
|
161
|
+
time.sleep(0.05)
|
|
162
|
+
|
|
163
|
+
assert any("pause" in c for c in calls)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_wear_mpris_resumes_when_reinserted(tmp_path, monkeypatch):
|
|
167
|
+
monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
|
|
168
|
+
profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
|
|
169
|
+
|
|
170
|
+
calls = []
|
|
171
|
+
|
|
172
|
+
def fake_run(cmd, **kw):
|
|
173
|
+
calls.append(cmd)
|
|
174
|
+
|
|
175
|
+
dev = _make_device()
|
|
176
|
+
with patch("nothing_app.protocol.subprocess.run", fake_run):
|
|
177
|
+
_set_wearing(dev, True, True)
|
|
178
|
+
_set_wearing(dev, False, False) # pause
|
|
179
|
+
_set_wearing(dev, True, False) # one bud back in → play
|
|
180
|
+
import time
|
|
181
|
+
|
|
182
|
+
time.sleep(0.05)
|
|
183
|
+
|
|
184
|
+
cmds = [c[-1] for c in calls]
|
|
185
|
+
assert "pause" in cmds
|
|
186
|
+
assert "play" in cmds
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_wear_mpris_disabled_by_default():
|
|
190
|
+
calls = []
|
|
191
|
+
|
|
192
|
+
def fake_run(cmd, **kw):
|
|
193
|
+
calls.append(cmd)
|
|
194
|
+
|
|
195
|
+
dev = _make_device()
|
|
196
|
+
with patch("nothing_app.protocol.subprocess.run", fake_run):
|
|
197
|
+
_set_wearing(dev, True, True)
|
|
198
|
+
_set_wearing(dev, False, False)
|
|
199
|
+
import time
|
|
200
|
+
|
|
201
|
+
time.sleep(0.05)
|
|
202
|
+
|
|
203
|
+
assert not calls, "wear_mpris is opt-in — should not fire with default prefs"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_wear_mpris_no_double_play(tmp_path, monkeypatch):
|
|
207
|
+
"""Wearing state bouncing while already in-ear should not trigger repeated plays."""
|
|
208
|
+
monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
|
|
209
|
+
profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
|
|
210
|
+
|
|
211
|
+
calls = []
|
|
212
|
+
|
|
213
|
+
def fake_run(cmd, **kw):
|
|
214
|
+
calls.append(cmd)
|
|
215
|
+
|
|
216
|
+
dev = _make_device()
|
|
217
|
+
with patch("nothing_app.protocol.subprocess.run", fake_run):
|
|
218
|
+
_set_wearing(dev, True, True)
|
|
219
|
+
_set_wearing(dev, False, False) # pause
|
|
220
|
+
_set_wearing(dev, True, False) # play
|
|
221
|
+
_set_wearing(dev, True, True) # still wearing — no extra play
|
|
222
|
+
import time
|
|
223
|
+
|
|
224
|
+
time.sleep(0.05)
|
|
225
|
+
|
|
226
|
+
play_calls = [c for c in calls if "play" in c]
|
|
227
|
+
assert len(play_calls) == 1
|
|
@@ -142,9 +142,9 @@ def test_nickname_does_not_affect_other_device():
|
|
|
142
142
|
# ── notify_prefs ──────────────────────────────────────────────────────────────
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def
|
|
145
|
+
def test_get_notify_prefs_defaults():
|
|
146
146
|
prefs = profiles.get_notify_prefs("AA:BB:CC:DD:EE:FF")
|
|
147
|
-
assert prefs == {"battery_low": True, "connect": True, "disconnect": True}
|
|
147
|
+
assert prefs == {"battery_low": True, "connect": True, "disconnect": True, "wear_mpris": False}
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def test_set_notify_prefs_roundtrip():
|
|
@@ -90,7 +90,7 @@ def test_parse_battery_updates_only_present_types():
|
|
|
90
90
|
payload = bytes([0x01, 0x03, 60])
|
|
91
91
|
dev._parse_battery(payload)
|
|
92
92
|
assert dev.state.right_battery == 60
|
|
93
|
-
assert dev.state.left_battery == -1
|
|
93
|
+
assert dev.state.left_battery == -1 # untouched default
|
|
94
94
|
assert dev.state.case_battery == -1
|
|
95
95
|
|
|
96
96
|
|
|
@@ -66,7 +66,9 @@ def test_load_bad_json_returns_defaults(tmp_path):
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def test_save_and_load_roundtrip():
|
|
69
|
-
t = Theme(
|
|
69
|
+
t = Theme(
|
|
70
|
+
accent="#3b82f6", bg_color="#1a1b26", window_opacity=0.8, card_opacity=0.5, blur=6, texture="dots"
|
|
71
|
+
)
|
|
70
72
|
save(t)
|
|
71
73
|
t2 = load()
|
|
72
74
|
assert t2 == t
|
|
@@ -319,7 +321,7 @@ def test_generate_css_bg_color_default_is_black():
|
|
|
319
321
|
|
|
320
322
|
def test_generate_css_custom_bg_used_in_app_background_gradient():
|
|
321
323
|
css = generate_css(Theme(bg_color="#1e1e2e"))
|
|
322
|
-
bg_block = css[css.index(".app-background {"):][:300]
|
|
324
|
+
bg_block = css[css.index(".app-background {") :][:300]
|
|
323
325
|
assert "1e1e2e" in bg_block or "background" in bg_block
|
|
324
326
|
|
|
325
327
|
|
|
@@ -344,29 +346,29 @@ def test_generate_css_blur_emits_filter():
|
|
|
344
346
|
|
|
345
347
|
def test_generate_css_blur_appears_in_app_background():
|
|
346
348
|
css = generate_css(Theme(blur=4))
|
|
347
|
-
bg_section = css[css.index(".app-background {"):]
|
|
348
|
-
first_block = bg_section[:bg_section.index("}") + 1]
|
|
349
|
+
bg_section = css[css.index(".app-background {") :]
|
|
350
|
+
first_block = bg_section[: bg_section.index("}") + 1]
|
|
349
351
|
assert "blur(4px)" in first_block
|
|
350
352
|
|
|
351
353
|
|
|
352
354
|
def test_generate_css_blur_not_in_nothing_page():
|
|
353
355
|
css = generate_css(Theme(blur=6))
|
|
354
|
-
page_section = css[css.index(".nothing-page {"):]
|
|
355
|
-
first_block = page_section[:page_section.index("}") + 1]
|
|
356
|
+
page_section = css[css.index(".nothing-page {") :]
|
|
357
|
+
first_block = page_section[: page_section.index("}") + 1]
|
|
356
358
|
assert "blur(" not in first_block
|
|
357
359
|
|
|
358
360
|
|
|
359
361
|
def test_generate_css_blur_not_in_device_card():
|
|
360
362
|
css = generate_css(Theme(blur=6))
|
|
361
|
-
card_section = css[css.index(".device-card {"):]
|
|
362
|
-
first_block = card_section[:card_section.index("}") + 1]
|
|
363
|
+
card_section = css[css.index(".device-card {") :]
|
|
364
|
+
first_block = card_section[: card_section.index("}") + 1]
|
|
363
365
|
assert "blur(" not in first_block
|
|
364
366
|
|
|
365
367
|
|
|
366
368
|
def test_generate_css_blur_not_in_settings_group():
|
|
367
369
|
css = generate_css(Theme(blur=6))
|
|
368
|
-
group_section = css[css.index(".settings-group {"):]
|
|
369
|
-
first_block = group_section[:group_section.index("}") + 1]
|
|
370
|
+
group_section = css[css.index(".settings-group {") :]
|
|
371
|
+
first_block = group_section[: group_section.index("}") + 1]
|
|
370
372
|
assert "blur(" not in first_block
|
|
371
373
|
|
|
372
374
|
|
|
@@ -392,25 +394,25 @@ def test_generate_css_card_opacity_scales_alpha():
|
|
|
392
394
|
|
|
393
395
|
def test_generate_css_texture_none_sets_no_image():
|
|
394
396
|
css = generate_css(Theme(texture="none"))
|
|
395
|
-
bg_block = css[css.index(".app-background {"):][:300]
|
|
397
|
+
bg_block = css[css.index(".app-background {") :][:300]
|
|
396
398
|
assert "background-image: none" in bg_block
|
|
397
399
|
|
|
398
400
|
|
|
399
401
|
def test_generate_css_texture_dots():
|
|
400
402
|
css = generate_css(Theme(texture="dots"))
|
|
401
|
-
bg_block = css[css.index(".app-background {"):][:400]
|
|
403
|
+
bg_block = css[css.index(".app-background {") :][:400]
|
|
402
404
|
assert "radial-gradient" in bg_block
|
|
403
405
|
|
|
404
406
|
|
|
405
407
|
def test_generate_css_texture_scanlines():
|
|
406
408
|
css = generate_css(Theme(texture="scanlines"))
|
|
407
|
-
bg_block = css[css.index(".app-background {"):][:500]
|
|
409
|
+
bg_block = css[css.index(".app-background {") :][:500]
|
|
408
410
|
assert "repeating-linear-gradient" in bg_block
|
|
409
411
|
|
|
410
412
|
|
|
411
413
|
def test_generate_css_texture_noise():
|
|
412
414
|
css = generate_css(Theme(texture="noise"))
|
|
413
|
-
bg_block = css[css.index(".app-background {"):][:700]
|
|
415
|
+
bg_block = css[css.index(".app-background {") :][:700]
|
|
414
416
|
assert "data:image/svg+xml;base64," in bg_block
|
|
415
417
|
|
|
416
418
|
|
|
File without changes
|
|
File without changes
|
{something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/release-dev.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/requires.txt
RENAMED
|
File without changes
|
{something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|