something-x-dev 1.8.2.dev27__py3-none-any.whl → 1.9.0.dev28__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.
- nothing_app/_version.py +2 -2
- nothing_app/application.py +49 -0
- nothing_app/pages/device.py +155 -1
- nothing_app/pages/theme.py +484 -0
- nothing_app/profiles.py +98 -10
- nothing_app/protocol.py +19 -16
- nothing_app/theme.py +312 -0
- nothing_app/window.py +74 -2
- {something_x_dev-1.8.2.dev27.dist-info → something_x_dev-1.9.0.dev28.dist-info}/METADATA +1 -1
- something_x_dev-1.9.0.dev28.dist-info/RECORD +23 -0
- something_x_dev-1.8.2.dev27.dist-info/RECORD +0 -21
- {something_x_dev-1.8.2.dev27.dist-info → something_x_dev-1.9.0.dev28.dist-info}/WHEEL +0 -0
- {something_x_dev-1.8.2.dev27.dist-info → something_x_dev-1.9.0.dev28.dist-info}/entry_points.txt +0 -0
- {something_x_dev-1.8.2.dev27.dist-info → something_x_dev-1.9.0.dev28.dist-info}/licenses/LICENSE +0 -0
- {something_x_dev-1.8.2.dev27.dist-info → something_x_dev-1.9.0.dev28.dist-info}/top_level.txt +0 -0
nothing_app/_version.py
CHANGED
|
@@ -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
|
nothing_app/application.py
CHANGED
|
@@ -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()
|
nothing_app/pages/device.py
CHANGED
|
@@ -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:
|