something-x-dev 1.2.3.dev1__py3-none-any.whl

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.
@@ -0,0 +1,210 @@
1
+ import gi
2
+
3
+ gi.require_version("Gtk", "4.0")
4
+ from gi.repository import Gtk, GLib, GObject
5
+
6
+ from ..bluetooth import BluetoothDevice, BluetoothManager
7
+
8
+
9
+ class DeviceRow(Gtk.Box):
10
+ def __init__(self, device: BluetoothDevice):
11
+ super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=14)
12
+ self.add_css_class("device-card")
13
+ self.device = device
14
+ self._build()
15
+
16
+ def _build(self):
17
+ icon_name = "audio-headphones-symbolic"
18
+ if "phone" in self.device.name.lower():
19
+ icon_name = "phone-symbolic"
20
+ elif "ear (stick)" in self.device.name.lower():
21
+ icon_name = "audio-input-microphone-symbolic"
22
+ icon = Gtk.Image.new_from_icon_name(icon_name)
23
+ icon.set_pixel_size(28)
24
+ icon.set_opacity(0.6)
25
+ self.append(icon)
26
+
27
+ text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
28
+ text_box.set_hexpand(True)
29
+
30
+ name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
31
+ name_lbl = Gtk.Label(label=self.device.name)
32
+ name_lbl.add_css_class("device-card-name")
33
+ name_lbl.set_xalign(0)
34
+ name_row.append(name_lbl)
35
+
36
+ if self.device.is_nothing:
37
+ badge = Gtk.Label(label="NOTHING")
38
+ badge.add_css_class("status-nothing")
39
+ name_row.append(badge)
40
+
41
+ text_box.append(name_row)
42
+
43
+ addr_lbl = Gtk.Label(label=self.device.address)
44
+ addr_lbl.add_css_class("device-card-address")
45
+ addr_lbl.set_xalign(0)
46
+ text_box.append(addr_lbl)
47
+
48
+ bottom_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
49
+ status_lbl = Gtk.Label(label="● Connected" if self.device.connected else "○ Disconnected")
50
+ status_lbl.add_css_class("status-connected" if self.device.connected else "status-disconnected")
51
+ status_lbl.set_xalign(0)
52
+ bottom_row.append(status_lbl)
53
+
54
+ if self.device.battery is not None:
55
+ bat_lbl = Gtk.Label(label=f" {self.device.battery}%")
56
+ bat_lbl.add_css_class("battery-pct")
57
+ bat_lbl.set_opacity(0.6)
58
+ bottom_row.append(bat_lbl)
59
+
60
+ text_box.append(bottom_row)
61
+ self.append(text_box)
62
+
63
+ arrow = Gtk.Image.new_from_icon_name("go-next-symbolic")
64
+ arrow.set_opacity(0.3)
65
+ self.append(arrow)
66
+
67
+
68
+ class HomePage(Gtk.Box):
69
+ __gsignals__ = {
70
+ "device-selected": (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
71
+ }
72
+
73
+ def __init__(self, bt_manager: BluetoothManager):
74
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
75
+ self._bt = bt_manager
76
+ self._scanning = False
77
+ self._build()
78
+ bt_manager.connect("devices-changed", self._on_devices_changed)
79
+ self._refresh_list()
80
+
81
+ def _build(self):
82
+ scroll = Gtk.ScrolledWindow()
83
+ scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
84
+ scroll.set_vexpand(True)
85
+
86
+ self._content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
87
+ self._content.add_css_class("nothing-page")
88
+ scroll.set_child(self._content)
89
+ self.append(scroll)
90
+
91
+ header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
92
+ header_box.set_margin_top(20)
93
+ header_box.set_margin_bottom(4)
94
+
95
+ dot = Gtk.Box()
96
+ dot.add_css_class("nothing-dot")
97
+ dot.set_valign(Gtk.Align.CENTER)
98
+ header_box.append(dot)
99
+
100
+ brand = Gtk.Label(label="Something X")
101
+ brand.add_css_class("section-label")
102
+ brand.set_margin_top(0)
103
+ brand.set_margin_bottom(0)
104
+ header_box.append(brand)
105
+ self._content.append(header_box)
106
+
107
+ self._nothing_label = Gtk.Label(label="MY DEVICES")
108
+ self._nothing_label.add_css_class("section-label")
109
+ self._nothing_label.set_xalign(0)
110
+ self._content.append(self._nothing_label)
111
+
112
+ self._nothing_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
113
+ self._content.append(self._nothing_list)
114
+
115
+ self._other_label = Gtk.Label(label="OTHER DEVICES")
116
+ self._other_label.add_css_class("section-label")
117
+ self._other_label.set_xalign(0)
118
+ self._content.append(self._other_label)
119
+
120
+ self._other_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
121
+ self._content.append(self._other_list)
122
+
123
+ self._empty_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
124
+ self._empty_box.set_margin_top(60)
125
+ self._empty_box.set_halign(Gtk.Align.CENTER)
126
+
127
+ empty_icon = Gtk.Image.new_from_icon_name("bluetooth-symbolic")
128
+ empty_icon.set_pixel_size(48)
129
+ empty_icon.set_opacity(0.15)
130
+ self._empty_box.append(empty_icon)
131
+
132
+ empty_title = Gtk.Label(label="NO DEVICES")
133
+ empty_title.add_css_class("empty-title")
134
+ self._empty_box.append(empty_title)
135
+
136
+ empty_sub = Gtk.Label(label="Pair a device via Bluetooth settings")
137
+ empty_sub.add_css_class("empty-subtitle")
138
+ self._empty_box.append(empty_sub)
139
+
140
+ self._content.append(self._empty_box)
141
+
142
+ scan_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
143
+ scan_row.set_margin_top(28)
144
+ scan_row.set_halign(Gtk.Align.CENTER)
145
+
146
+ self._scan_btn = Gtk.Button(label="SCAN FOR DEVICES")
147
+ self._scan_btn.add_css_class("scan-button")
148
+ self._scan_btn.connect("clicked", self._on_scan_clicked)
149
+ scan_row.append(self._scan_btn)
150
+ self._content.append(scan_row)
151
+
152
+ self._bt_warning = Gtk.Label(label="⚠ Bluetooth unavailable — is bluetoothd running?")
153
+ self._bt_warning.add_css_class("empty-subtitle")
154
+ self._bt_warning.set_margin_top(8)
155
+ self._bt_warning.set_halign(Gtk.Align.CENTER)
156
+ self._content.append(self._bt_warning)
157
+ self._bt_warning.set_visible(not self._bt.available)
158
+
159
+ def _clear_list(self, box: Gtk.Box):
160
+ while True:
161
+ child = box.get_first_child()
162
+ if child is None:
163
+ break
164
+ box.remove(child)
165
+
166
+ def _refresh_list(self):
167
+ devices = self._bt.get_all()
168
+ nothing = [d for d in devices if d.is_nothing]
169
+ others = [d for d in devices if not d.is_nothing]
170
+
171
+ self._clear_list(self._nothing_list)
172
+ self._clear_list(self._other_list)
173
+
174
+ for dev in nothing:
175
+ self._nothing_list.append(self._make_row(dev))
176
+
177
+ for dev in others:
178
+ self._other_list.append(self._make_row(dev))
179
+
180
+ self._nothing_label.set_visible(bool(nothing))
181
+ self._other_label.set_visible(bool(others))
182
+ self._empty_box.set_visible(not devices)
183
+
184
+ def _make_row(self, device: BluetoothDevice) -> Gtk.Button:
185
+ btn = Gtk.Button()
186
+ btn.set_has_frame(False)
187
+ btn.add_css_class("device-row-btn")
188
+ row = DeviceRow(device)
189
+ btn.set_child(row)
190
+ btn.connect("clicked", lambda _b, d=device: self.emit("device-selected", d))
191
+ return btn
192
+
193
+ def _on_devices_changed(self, _manager):
194
+ self._refresh_list()
195
+
196
+ def _on_scan_clicked(self, _btn):
197
+ if self._scanning:
198
+ return
199
+ self._scanning = True
200
+ self._scan_btn.set_label("SCANNING…")
201
+ self._scan_btn.set_sensitive(False)
202
+ self._bt.start_discovery()
203
+ GLib.timeout_add_seconds(30, self._scan_done)
204
+
205
+ def _scan_done(self):
206
+ self._scanning = False
207
+ self._scan_btn.set_label("SCAN FOR DEVICES")
208
+ self._scan_btn.set_sensitive(True)
209
+ self._refresh_list()
210
+ return False
@@ -0,0 +1,41 @@
1
+ import json
2
+ import os
3
+
4
+ _DIR = os.path.expanduser("~/.config/something-x")
5
+ _PROFILES_FILE = os.path.join(_DIR, "profiles.json")
6
+ _LAST_DEV_FILE = os.path.join(_DIR, "last_device")
7
+
8
+
9
+ def load(address: str) -> dict:
10
+ try:
11
+ with open(_PROFILES_FILE) as f:
12
+ return json.load(f).get(address, {})
13
+ except (FileNotFoundError, json.JSONDecodeError):
14
+ return {}
15
+
16
+
17
+ def save(address: str, anc_mode: int, eq_preset: str):
18
+ os.makedirs(_DIR, exist_ok=True)
19
+ data = {}
20
+ try:
21
+ with open(_PROFILES_FILE) as f:
22
+ data = json.load(f)
23
+ except (FileNotFoundError, json.JSONDecodeError):
24
+ pass
25
+ data[address] = {"anc": anc_mode, "eq": eq_preset}
26
+ with open(_PROFILES_FILE, "w") as f:
27
+ json.dump(data, f, indent=2)
28
+
29
+
30
+ def get_last_device() -> str | None:
31
+ try:
32
+ addr = open(_LAST_DEV_FILE).read().strip()
33
+ return addr or None
34
+ except FileNotFoundError:
35
+ return None
36
+
37
+
38
+ def set_last_device(address: str):
39
+ os.makedirs(_DIR, exist_ok=True)
40
+ with open(_LAST_DEV_FILE, "w") as f:
41
+ f.write(address)