something-x-dev 1.8.2.dev26__tar.gz → 1.9.0.dev28__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.8.2.dev26 → something_x_dev-1.9.0.dev28}/PKG-INFO +1 -1
- something_x_dev-1.9.0.dev28/ROADMAP.md +107 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/_version.py +2 -2
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/application.py +49 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/pages/device.py +155 -1
- something_x_dev-1.9.0.dev28/nothing_app/pages/theme.py +484 -0
- something_x_dev-1.9.0.dev28/nothing_app/profiles.py +129 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/protocol.py +19 -16
- something_x_dev-1.9.0.dev28/nothing_app/theme.py +312 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/window.py +74 -2
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/PKG-INFO +1 -1
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/SOURCES.txt +5 -1
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/tests/conftest.py +2 -0
- something_x_dev-1.9.0.dev28/tests/test_notifications.py +133 -0
- something_x_dev-1.9.0.dev28/tests/test_profiles.py +267 -0
- something_x_dev-1.9.0.dev28/tests/test_theme.py +454 -0
- something_x_dev-1.8.2.dev26/ROADMAP.md +0 -69
- something_x_dev-1.8.2.dev26/nothing_app/profiles.py +0 -41
- something_x_dev-1.8.2.dev26/tests/test_profiles.py +0 -90
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/.github/CODEOWNERS +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/.github/workflows/ci.yml +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/.github/workflows/release-dev.yml +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/.github/workflows/release.yml +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/.gitignore +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/DEVICES.md +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/LICENSE +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/PKGBUILD +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/README.md +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/cliff.toml +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/docs/RELEASING.md +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/docs/docs.html +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/docs/index.html +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/flake.nix +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/__init__.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/bluetooth.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/data/__init__.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/data/com.something.x.omarchy.desktop +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/data/style.css +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/pages/__init__.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/pages/home.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/splash.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/nothing_app/tray.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/pyproject.toml +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/setup.cfg +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/dependency_links.txt +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/entry_points.txt +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/requires.txt +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/something_x_dev.egg-info/top_level.txt +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/somethingx +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/tests/__init__.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/tests/test_bluetooth.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/tests/test_crc.py +0 -0
- {something_x_dev-1.8.2.dev26 → something_x_dev-1.9.0.dev28}/tests/test_protocol.py +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Something X — Roadmap
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Current release — v1.8
|
|
6
|
+
|
|
7
|
+
### Shipped
|
|
8
|
+
- [x] **GitHub Pages** — project landing page + full documentation site (`docs/`)
|
|
9
|
+
- [x] **AUR package** — Arch users can install without pip
|
|
10
|
+
- [x] **NixOS flake** — `flake.nix` for Nix users
|
|
11
|
+
- [x] **`.desktop` file** — ships and auto-installs so the app appears in Walker/Rofi/GNOME launcher
|
|
12
|
+
- [x] **Low battery notification** — `notify-send` when any bud drops below 20 %
|
|
13
|
+
- [x] **Background mode** — closing the window hides it; relaunch shows it again (Gio single-instance)
|
|
14
|
+
- [x] **Per-device profiles** — ANC + EQ saved per device address to `~/.config/something-x/profiles.json`, restored on reconnect
|
|
15
|
+
- [x] **CLI quick-toggle** — `something-x --anc off|on|transparency`, `something-x --eq bass`, `something-x --battery`
|
|
16
|
+
- [x] **System tray icon** — battery on hover via StatusNotifierItem (Waybar/SNI)
|
|
17
|
+
- [x] **Wearing detection display** — L/R in-ear status shown on the earbud visual
|
|
18
|
+
- [x] **Auto-connect RFCOMM** — connects as soon as BlueZ reports the device connected
|
|
19
|
+
- [x] **Async BlueZ** — native `Gio` async D-Bus, no `dbus-python` dependency
|
|
20
|
+
- [x] **Unit tests** — 65 tests covering CRC, protocol parsing, battery dedup, profiles, BluetoothManager
|
|
21
|
+
- [x] **Debug mode** — `SOMETHING_X_DEBUG=1` dumps raw RFCOMM frames for contributors
|
|
22
|
+
- [x] **Graceful ANC mode detection** — infer supported modes from RFCOMM response; hide unsupported buttons
|
|
23
|
+
- [x] **Dynamic versioning** — version sourced from git tags at build time
|
|
24
|
+
- [x] **Liquid glass dark UI** — overhauled aesthetic for the app window
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1.9 — Quality of life
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
- [x] **Device nickname** — rename a paired device in the UI; stored in profiles
|
|
32
|
+
- [x] **Profile import / export** — share `.json` profile files between machines or with other users
|
|
33
|
+
- [ ] **Wear-detect MPRIS actions** — pause media when both buds are removed; resume when reinserted (opt-in)
|
|
34
|
+
- [x] **Notification preferences** — per-event toggles (battery low, connect, disconnect) in settings
|
|
35
|
+
- [x] **Theming** — user-selectable accent color and light/dark mode toggle; theme stored in config
|
|
36
|
+
|
|
37
|
+
### Tech
|
|
38
|
+
- [x] **Improved test coverage** — profiles round-trip, nickname, notify prefs, notification gating (91 tests total)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1.10 — Desktop integration
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
- [ ] **D-Bus API** — expose ANC, EQ, battery, and connect/disconnect over a session-bus interface so scripts and status bars can consume it without spawning a CLI process
|
|
46
|
+
- [ ] **Global keybind support** — register a configurable shortcut (e.g. `Super+F1`) to cycle ANC modes without opening the window
|
|
47
|
+
- [ ] **Quick-settings overlay** — small floating panel triggered by keybind or tray click; ANC toggle + battery at a glance
|
|
48
|
+
- [ ] **Waybar module documentation** — sample `custom/somethingx` block wired to the D-Bus API
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 1.11 — Multi-device
|
|
53
|
+
|
|
54
|
+
### Features
|
|
55
|
+
- [ ] **Multiple devices simultaneously** — open a page per device, manage both at once without switching
|
|
56
|
+
- [ ] **Battery overview** — compact summary card on the home page showing all connected devices and their battery levels
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 1.12 — EQ overhaul
|
|
61
|
+
|
|
62
|
+
### Features
|
|
63
|
+
- [ ] **Visual EQ graph** — interactive SVG/Cairo curve instead of preset pills
|
|
64
|
+
- [ ] **Custom EQ presets** — per-band sliders, save with a user-defined name, stored in profiles
|
|
65
|
+
- [ ] **Preset sharing** — export / import individual EQ presets as `.json`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 1.13 — Protocol depth
|
|
70
|
+
|
|
71
|
+
### Features
|
|
72
|
+
- [ ] **Touch controls remapping** — reassign tap/hold actions if the protocol exposes it (command IDs found in APK, not yet wired up)
|
|
73
|
+
- [ ] **Firmware version display** — read firmware string from RFCOMM and show it in the device page
|
|
74
|
+
- [ ] **Firmware update check** — compare device firmware against latest known version, show a badge when outdated
|
|
75
|
+
|
|
76
|
+
### Protocol / devices
|
|
77
|
+
- [ ] **CMF Buds / Buds Pro validation** — confirm command IDs and ship explicit support
|
|
78
|
+
- [ ] **Nothing Ear (1) / (a) / (stick) community testing** — collect field reports, fix edge cases
|
|
79
|
+
- [ ] **Structured debug logger** — `SOMETHING_X_DEBUG=1` writes a structured `.jsonl` capture file for easier issue reproduction
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 1.14 — Additional devices
|
|
84
|
+
|
|
85
|
+
### Features
|
|
86
|
+
- [ ] **Nothing Ear (open) support** — ANC not applicable; validate battery and EQ commands
|
|
87
|
+
- [ ] **CMF Watch support** — separate protocol; requires fresh APK reverse-engineering
|
|
88
|
+
- [ ] **Nothing Phone glyph integration** — notification mirroring and glyph lighting (depends on BlueZ + glyph API availability)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Long term / ideas
|
|
93
|
+
|
|
94
|
+
These have no version target yet — they need either upstream work, a contributor, or significant scoping.
|
|
95
|
+
|
|
96
|
+
- [ ] **GNOME Shell extension** — battery indicator in the GNOME top bar
|
|
97
|
+
- [ ] **KDE Plasma widget** — same idea for KDE users
|
|
98
|
+
- [ ] **macOS port** — replace BlueZ D-Bus with CoreBluetooth via `pyobjc` (long shot)
|
|
99
|
+
- [ ] **Battery history graph** — plot battery level over time, persisted across sessions
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Won't do
|
|
104
|
+
|
|
105
|
+
- **Windows** — no BlueZ; RFCOMM access requires a completely different stack
|
|
106
|
+
- **Wayland screen capture control** — out of scope
|
|
107
|
+
- **Streaming / media playback control** — MPRIS already handles this system-wide
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.9.0.dev28'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 0, 'dev28')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -50,6 +50,9 @@ class SomethingXApplication(Adw.Application):
|
|
|
50
50
|
self._splash: SplashScreen | None = None
|
|
51
51
|
self._window: SomethingXWindow | None = None
|
|
52
52
|
self._tray: SomethingXTray | None = None
|
|
53
|
+
self._theme_provider: Gtk.CssProvider | None = None
|
|
54
|
+
self._current_theme = None
|
|
55
|
+
self._theme_save_id: int | None = None
|
|
53
56
|
self.connect("activate", self._on_activate)
|
|
54
57
|
|
|
55
58
|
def _on_activate(self, _app):
|
|
@@ -61,6 +64,7 @@ class SomethingXApplication(Adw.Application):
|
|
|
61
64
|
_install_desktop_file()
|
|
62
65
|
Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK)
|
|
63
66
|
self._load_css()
|
|
67
|
+
self._load_theme_css()
|
|
64
68
|
self._bt = BluetoothManager()
|
|
65
69
|
splash = SplashScreen(on_done=self._on_splash_done)
|
|
66
70
|
splash.set_application(self)
|
|
@@ -72,6 +76,8 @@ class SomethingXApplication(Adw.Application):
|
|
|
72
76
|
win = SomethingXWindow(bt_manager=self._bt, application=self)
|
|
73
77
|
win.connect("close-request", self._on_window_close)
|
|
74
78
|
self._window = win
|
|
79
|
+
if self._current_theme is not None:
|
|
80
|
+
win.set_opacity(max(0.05, min(1.0, self._current_theme.window_opacity)))
|
|
75
81
|
self._tray = SomethingXTray(self._bt, on_show_window=win.present)
|
|
76
82
|
win.present()
|
|
77
83
|
if self._splash:
|
|
@@ -93,6 +99,49 @@ class SomethingXApplication(Adw.Application):
|
|
|
93
99
|
)
|
|
94
100
|
return True # prevent default close/destroy
|
|
95
101
|
|
|
102
|
+
def _load_theme_css(self):
|
|
103
|
+
from . import theme as _theme_mod
|
|
104
|
+
|
|
105
|
+
self._current_theme = _theme_mod.load()
|
|
106
|
+
self._theme_provider = Gtk.CssProvider()
|
|
107
|
+
css = _theme_mod.generate_css(self._current_theme)
|
|
108
|
+
try:
|
|
109
|
+
self._theme_provider.load_from_string(css)
|
|
110
|
+
except AttributeError:
|
|
111
|
+
self._theme_provider.load_from_data(css.encode())
|
|
112
|
+
|
|
113
|
+
display = Gdk.Display.get_default()
|
|
114
|
+
if display:
|
|
115
|
+
Gtk.StyleContext.add_provider_for_display(
|
|
116
|
+
display,
|
|
117
|
+
self._theme_provider,
|
|
118
|
+
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def apply_theme(self, t):
|
|
122
|
+
from . import theme as _theme_mod
|
|
123
|
+
|
|
124
|
+
self._current_theme = t
|
|
125
|
+
css = _theme_mod.generate_css(t)
|
|
126
|
+
try:
|
|
127
|
+
self._theme_provider.load_from_string(css)
|
|
128
|
+
except AttributeError:
|
|
129
|
+
self._theme_provider.load_from_data(css.encode())
|
|
130
|
+
|
|
131
|
+
if self._window is not None:
|
|
132
|
+
self._window.set_opacity(max(0.05, min(1.0, t.window_opacity)))
|
|
133
|
+
|
|
134
|
+
if self._theme_save_id is not None:
|
|
135
|
+
GLib.source_remove(self._theme_save_id)
|
|
136
|
+
self._theme_save_id = GLib.timeout_add(500, self._flush_theme_save)
|
|
137
|
+
|
|
138
|
+
def _flush_theme_save(self):
|
|
139
|
+
from . import theme as _theme_mod
|
|
140
|
+
|
|
141
|
+
_theme_mod.save(self._current_theme)
|
|
142
|
+
self._theme_save_id = None
|
|
143
|
+
return False
|
|
144
|
+
|
|
96
145
|
def _load_css(self):
|
|
97
146
|
provider = Gtk.CssProvider()
|
|
98
147
|
css = _css_path()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import math
|
|
2
3
|
import re
|
|
3
4
|
import subprocess
|
|
@@ -8,10 +9,11 @@ import gi
|
|
|
8
9
|
gi.require_version("Gtk", "4.0")
|
|
9
10
|
gi.require_version("Pango", "1.0")
|
|
10
11
|
gi.require_version("PangoCairo", "1.0")
|
|
11
|
-
from gi.repository import Gtk, GLib, PangoCairo
|
|
12
|
+
from gi.repository import Gtk, GLib, PangoCairo, Gio
|
|
12
13
|
|
|
13
14
|
from ..bluetooth import BluetoothDevice, BluetoothManager
|
|
14
15
|
from ..protocol import NothingDevice, ANCMode, EQ_PRESETS
|
|
16
|
+
from .. import profiles
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def _mono_font() -> str:
|
|
@@ -487,12 +489,58 @@ class DevicePage(Gtk.Box):
|
|
|
487
489
|
|
|
488
490
|
page.append(settings_group)
|
|
489
491
|
|
|
492
|
+
page.append(_section("NOTIFICATIONS"))
|
|
493
|
+
|
|
494
|
+
notif_group = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
|
495
|
+
notif_group.add_css_class("settings-group")
|
|
496
|
+
notif_group.set_margin_bottom(4)
|
|
497
|
+
|
|
498
|
+
notify_prefs = profiles.get_notify_prefs(self._bt_device.address)
|
|
499
|
+
|
|
500
|
+
self._notif_battery_switch = Gtk.Switch()
|
|
501
|
+
self._notif_battery_switch.set_active(notify_prefs.get("battery_low", True))
|
|
502
|
+
self._notif_battery_switch.set_valign(Gtk.Align.CENTER)
|
|
503
|
+
self._notif_battery_switch.connect("state-set", self._on_notif_battery_toggled)
|
|
504
|
+
notif_group.append(
|
|
505
|
+
_settings_row("Battery Low", "Alert when battery drops below 20%", self._notif_battery_switch)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
self._notif_connect_switch = Gtk.Switch()
|
|
509
|
+
self._notif_connect_switch.set_active(notify_prefs.get("connect", True))
|
|
510
|
+
self._notif_connect_switch.set_valign(Gtk.Align.CENTER)
|
|
511
|
+
self._notif_connect_switch.connect("state-set", self._on_notif_connect_toggled)
|
|
512
|
+
notif_group.append(
|
|
513
|
+
_settings_row("Connected", "Alert when device connects", self._notif_connect_switch)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
self._notif_disconnect_switch = Gtk.Switch()
|
|
517
|
+
self._notif_disconnect_switch.set_active(notify_prefs.get("disconnect", True))
|
|
518
|
+
self._notif_disconnect_switch.set_valign(Gtk.Align.CENTER)
|
|
519
|
+
self._notif_disconnect_switch.connect("state-set", self._on_notif_disconnect_toggled)
|
|
520
|
+
notif_group.append(
|
|
521
|
+
_settings_row("Disconnected", "Alert when device disconnects", self._notif_disconnect_switch)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
page.append(notif_group)
|
|
525
|
+
|
|
490
526
|
page.append(_section("DEVICE INFO"))
|
|
491
527
|
|
|
492
528
|
info_group = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
|
493
529
|
info_group.add_css_class("settings-group")
|
|
494
530
|
info_group.set_margin_bottom(4)
|
|
495
531
|
|
|
532
|
+
nick_entry = Gtk.Entry()
|
|
533
|
+
nick_entry.set_hexpand(True)
|
|
534
|
+
nick_entry.set_valign(Gtk.Align.CENTER)
|
|
535
|
+
saved_nick = profiles.get_nickname(self._bt_device.address)
|
|
536
|
+
if saved_nick:
|
|
537
|
+
nick_entry.set_text(saved_nick)
|
|
538
|
+
nick_entry.set_placeholder_text(self._bt_device.name)
|
|
539
|
+
nick_entry.connect("activate", self._on_nickname_activate)
|
|
540
|
+
nick_entry.connect("notify::has-focus", self._on_nickname_focus_lost)
|
|
541
|
+
self._nick_entry = nick_entry
|
|
542
|
+
info_group.append(_settings_row("Nickname", right_widget=nick_entry))
|
|
543
|
+
|
|
496
544
|
self._fw_label = Gtk.Label(label="—")
|
|
497
545
|
self._fw_label.add_css_class("info-value")
|
|
498
546
|
self._fw_label.set_xalign(1)
|
|
@@ -508,6 +556,23 @@ class DevicePage(Gtk.Box):
|
|
|
508
556
|
addr_val.set_xalign(1)
|
|
509
557
|
info_group.append(_settings_row("Address", right_widget=addr_val))
|
|
510
558
|
|
|
559
|
+
io_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
560
|
+
io_box.set_margin_top(4)
|
|
561
|
+
|
|
562
|
+
export_btn = Gtk.Button(label="Export Profile")
|
|
563
|
+
export_btn.add_css_class("settings-row-action")
|
|
564
|
+
export_btn.set_hexpand(True)
|
|
565
|
+
export_btn.connect("clicked", self._on_export_clicked)
|
|
566
|
+
|
|
567
|
+
import_btn = Gtk.Button(label="Import Profile")
|
|
568
|
+
import_btn.add_css_class("settings-row-action")
|
|
569
|
+
import_btn.set_hexpand(True)
|
|
570
|
+
import_btn.connect("clicked", self._on_import_clicked)
|
|
571
|
+
|
|
572
|
+
io_box.append(export_btn)
|
|
573
|
+
io_box.append(import_btn)
|
|
574
|
+
info_group.append(io_box)
|
|
575
|
+
|
|
511
576
|
page.append(info_group)
|
|
512
577
|
|
|
513
578
|
self._sync_anc_ui(ANCMode.OFF)
|
|
@@ -634,6 +699,95 @@ class DevicePage(Gtk.Box):
|
|
|
634
699
|
else:
|
|
635
700
|
btn.remove_css_class("active")
|
|
636
701
|
|
|
702
|
+
def _save_nickname(self):
|
|
703
|
+
if not hasattr(self, "_nick_entry"):
|
|
704
|
+
return
|
|
705
|
+
text = self._nick_entry.get_text().strip()
|
|
706
|
+
profiles.set_nickname(self._bt_device.address, text or None)
|
|
707
|
+
|
|
708
|
+
def _on_nickname_activate(self, _entry):
|
|
709
|
+
self._save_nickname()
|
|
710
|
+
|
|
711
|
+
def _on_nickname_focus_lost(self, entry, _param):
|
|
712
|
+
if not entry.has_focus():
|
|
713
|
+
self._save_nickname()
|
|
714
|
+
|
|
715
|
+
def _on_notif_battery_toggled(self, _switch, state: bool):
|
|
716
|
+
profiles.set_notify_prefs(self._bt_device.address, {"battery_low": state})
|
|
717
|
+
return False
|
|
718
|
+
|
|
719
|
+
def _on_notif_connect_toggled(self, _switch, state: bool):
|
|
720
|
+
profiles.set_notify_prefs(self._bt_device.address, {"connect": state})
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
def _on_notif_disconnect_toggled(self, _switch, state: bool):
|
|
724
|
+
profiles.set_notify_prefs(self._bt_device.address, {"disconnect": state})
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
def _on_export_clicked(self, _btn):
|
|
728
|
+
dialog = Gtk.FileChooserNative(
|
|
729
|
+
title="Export Profile",
|
|
730
|
+
transient_for=self.get_root(),
|
|
731
|
+
action=Gtk.FileChooserAction.SAVE,
|
|
732
|
+
)
|
|
733
|
+
dialog.set_current_name(f"something-x-{self._bt_device.address.replace(':', '-')}.json")
|
|
734
|
+
dialog.connect("response", self._on_export_response, dialog)
|
|
735
|
+
dialog.show()
|
|
736
|
+
|
|
737
|
+
def _on_export_response(self, _dialog, response: int, dialog: Gtk.FileChooserNative):
|
|
738
|
+
if response != Gtk.ResponseType.ACCEPT:
|
|
739
|
+
return
|
|
740
|
+
gfile = dialog.get_file()
|
|
741
|
+
if not gfile:
|
|
742
|
+
return
|
|
743
|
+
path = gfile.get_path()
|
|
744
|
+
data = profiles.export_profile(self._bt_device.address)
|
|
745
|
+
try:
|
|
746
|
+
with open(path, "w") as f:
|
|
747
|
+
json.dump(data, f, indent=2)
|
|
748
|
+
except Exception as exc:
|
|
749
|
+
print(f"[device] export failed: {exc}")
|
|
750
|
+
|
|
751
|
+
def _on_import_clicked(self, _btn):
|
|
752
|
+
dialog = Gtk.FileChooserNative(
|
|
753
|
+
title="Import Profile",
|
|
754
|
+
transient_for=self.get_root(),
|
|
755
|
+
action=Gtk.FileChooserAction.OPEN,
|
|
756
|
+
)
|
|
757
|
+
filt = Gtk.FileFilter()
|
|
758
|
+
filt.set_name("JSON files")
|
|
759
|
+
filt.add_mime_type("application/json")
|
|
760
|
+
filt.add_pattern("*.json")
|
|
761
|
+
dialog.add_filter(filt)
|
|
762
|
+
dialog.connect("response", self._on_import_response, dialog)
|
|
763
|
+
dialog.show()
|
|
764
|
+
|
|
765
|
+
def _on_import_response(self, _dialog, response: int, dialog: Gtk.FileChooserNative):
|
|
766
|
+
if response != Gtk.ResponseType.ACCEPT:
|
|
767
|
+
return
|
|
768
|
+
gfile = dialog.get_file()
|
|
769
|
+
if not gfile:
|
|
770
|
+
return
|
|
771
|
+
path = gfile.get_path()
|
|
772
|
+
try:
|
|
773
|
+
with open(path) as f:
|
|
774
|
+
data = json.load(f)
|
|
775
|
+
profiles.import_profile(self._bt_device.address, data)
|
|
776
|
+
self._reload_notif_switches()
|
|
777
|
+
if hasattr(self, "_nick_entry"):
|
|
778
|
+
nick = profiles.get_nickname(self._bt_device.address)
|
|
779
|
+
self._nick_entry.set_text(nick or "")
|
|
780
|
+
except Exception as exc:
|
|
781
|
+
print(f"[device] import failed: {exc}")
|
|
782
|
+
|
|
783
|
+
def _reload_notif_switches(self):
|
|
784
|
+
if not hasattr(self, "_notif_battery_switch"):
|
|
785
|
+
return
|
|
786
|
+
prefs = profiles.get_notify_prefs(self._bt_device.address)
|
|
787
|
+
self._notif_battery_switch.set_active(prefs.get("battery_low", True))
|
|
788
|
+
self._notif_connect_switch.set_active(prefs.get("connect", True))
|
|
789
|
+
self._notif_disconnect_switch.set_active(prefs.get("disconnect", True))
|
|
790
|
+
|
|
637
791
|
def _on_anc_clicked(self, _btn, mode: int):
|
|
638
792
|
self._sync_anc_ui(mode)
|
|
639
793
|
if self._nothing_dev:
|