Skip to content

Commit b57e5dc

Browse files
committed
Add AI radio assistant UI
1 parent f67d32a commit b57e5dc

30 files changed

Lines changed: 4542 additions & 361 deletions

examples/macos_native_titlebar.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""macOS PyQt6: native traffic-light buttons, hidden titlebar.
2+
3+
Run: uv run --extra qt python examples/macos_native_titlebar.py
4+
"""
5+
6+
import ctypes
7+
import sys
8+
from ctypes import c_bool, c_char_p, c_long, c_ulong, c_void_p
9+
10+
from PyQt6.QtCore import QTimer, Qt
11+
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
12+
13+
14+
objc = ctypes.cdll.LoadLibrary("/usr/lib/libobjc.A.dylib")
15+
objc.sel_registerName.restype = c_void_p
16+
objc.sel_registerName.argtypes = [c_char_p]
17+
18+
19+
def sel(name):
20+
return objc.sel_registerName(name.encode())
21+
22+
23+
def msg(receiver, selector, restype=c_void_p, argtypes=(), *args):
24+
send = objc.objc_msgSend
25+
send.restype = restype
26+
send.argtypes = [c_void_p, c_void_p, *argtypes]
27+
return send(receiver, sel(selector), *args)
28+
29+
30+
def expand_client_area(widget):
31+
for name in ("ExpandedClientAreaHint", "NoTitleBarBackgroundHint"):
32+
flag = getattr(Qt.WindowType, name, None)
33+
if flag is not None:
34+
widget.setWindowFlag(flag, True)
35+
attr = getattr(Qt.WidgetAttribute, "WA_ContentsMarginsRespectsSafeArea", None)
36+
if attr is not None:
37+
widget.setAttribute(attr, False)
38+
39+
40+
def make_titlebar_transparent(widget):
41+
ns_window = msg(c_void_p(int(widget.winId())), "window")
42+
style = msg(ns_window, "styleMask", c_ulong)
43+
style |= (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 15)
44+
msg(ns_window, "setStyleMask:", None, (c_ulong,), style)
45+
msg(ns_window, "setTitleVisibility:", None, (c_long,), 1)
46+
msg(ns_window, "setTitlebarAppearsTransparent:", None, (c_bool,), True)
47+
msg(ns_window, "setMovableByWindowBackground:", None, (c_bool,), True)
48+
49+
50+
class Toolbar(QWidget):
51+
def __init__(self):
52+
super().__init__()
53+
self.setFixedHeight(38)
54+
layout = QHBoxLayout(self)
55+
layout.setContentsMargins(86, 0, 12, 0)
56+
layout.setSpacing(8)
57+
layout.addWidget(QLabel("Toolbar at y=0"), 0, Qt.AlignmentFlag.AlignTop)
58+
layout.addStretch()
59+
for text in ("Back", "Forward", "Search"):
60+
button = QPushButton(text)
61+
button.setFixedHeight(28)
62+
layout.addWidget(button, 0, Qt.AlignmentFlag.AlignTop)
63+
64+
def mousePressEvent(self, event):
65+
window = self.window().windowHandle()
66+
if event.button() == Qt.MouseButton.LeftButton and window is not None:
67+
if window.startSystemMove():
68+
event.accept()
69+
return
70+
super().mousePressEvent(event)
71+
72+
73+
def main():
74+
if sys.platform != "darwin":
75+
print("This demo is macOS-only.")
76+
return 1
77+
78+
app = QApplication(sys.argv)
79+
window = QWidget()
80+
expand_client_area(window)
81+
window.setWindowTitle("Hidden titlebar")
82+
window.resize(720, 420)
83+
84+
layout = QVBoxLayout(window)
85+
layout.setContentsMargins(0, 0, 0, 0)
86+
layout.setSpacing(0)
87+
layout.addWidget(Toolbar())
88+
89+
body = QLabel("Native buttons are kept; the titlebar is not visible.")
90+
body.setAlignment(Qt.AlignmentFlag.AlignCenter)
91+
layout.addWidget(body, 1)
92+
93+
window.show()
94+
QTimer.singleShot(0, lambda: make_titlebar_transparent(window))
95+
return app.exec()
96+
97+
98+
if __name__ == "__main__":
99+
raise SystemExit(main())

feeluown/ai/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# flake8: noqa
22

3-
from .copilot import AISongModel, Copilot, AISongMatcher
3+
from .copilot import SongSuggestion, Copilot, SongSuggestionMatcher
44
from .ai import AI
5+
from .radio import AIRadioSession

feeluown/ai/ai.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from typing import TYPE_CHECKING, Iterable, Optional
2+
13
from feeluown.app import App
24
from feeluown.ai.copilot import Copilot
5+
from feeluown.ai.radio import AIRadioSession
6+
7+
if TYPE_CHECKING:
8+
from feeluown.library import BriefSongModel
39

410

511
# FIXME: Other packages should only import things from feeluown.ai
@@ -10,6 +16,36 @@ class AI:
1016
def __init__(self, app: App):
1117
self._app = app
1218
self._copilot = Copilot(self._app)
19+
self.radio = None
1320

1421
def get_copilot(self):
1522
return self._copilot
23+
24+
def get_active_radio(self) -> Optional["AIRadioSession"]:
25+
if self.radio is not None and self.radio.is_active:
26+
return self.radio
27+
return None
28+
29+
def activate_radio(
30+
self,
31+
initial_songs: Optional[Iterable["BriefSongModel"]] = None,
32+
reset=True,
33+
) -> AIRadioSession:
34+
"""Activate AI Radio and return the active session."""
35+
active_radio = self.get_active_radio()
36+
if active_radio is not None:
37+
return active_radio
38+
39+
radio = AIRadioSession(self._app, initial_songs=initial_songs)
40+
radio.activate(reset=reset)
41+
return radio
42+
43+
def deactivate_radio(self) -> bool:
44+
"""Deactivate AI Radio. Return True when a session was active."""
45+
radio = self.radio
46+
if radio is None:
47+
return False
48+
49+
was_active = radio.is_active
50+
radio.deactivate()
51+
return was_active

0 commit comments

Comments
 (0)