• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

feeluown / FeelUOwn / 13067922984

31 Jan 2025 06:54AM UTC coverage: 56.468% (-0.3%) from 56.733%
13067922984

Pull #899

github

web-flow
Merge acce92d2d into d9f39dc04
Pull Request #899: [feat](*): AI based radio

72 of 186 new or added lines in 8 files covered. (38.71%)

5 existing lines in 1 file now uncovered.

10027 of 17757 relevant lines covered (56.47%)

0.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.38
/feeluown/gui/uimain/playlist_overlay.py
1
from PyQt5.QtCore import Qt, QRect, QEvent
1✔
2
from PyQt5.QtWidgets import (
1✔
3
    QWidget, QStackedLayout, QVBoxLayout, QHBoxLayout,
4
    QApplication,
5
)
6
from PyQt5.QtGui import (
1✔
7
    QColor, QLinearGradient, QPalette, QPainter,
8
)
9

10
from feeluown.player import PlaybackMode, SongsRadio, AIRadio, AI_RADIO_SUPPORTED
1✔
11
from feeluown.gui.helpers import fetch_cover_wrapper, esc_hide_widget
1✔
12
from feeluown.gui.components.player_playlist import PlayerPlaylistView
1✔
13
from feeluown.gui.widgets.textbtn import TextButton
1✔
14
from feeluown.gui.widgets.tabbar import TabBar
1✔
15
from feeluown.gui.widgets.song_minicard_list import (
1✔
16
    SongMiniCardListView,
17
    SongMiniCardListModel,
18
    SongMiniCardListDelegate,
19
)
20
from feeluown.utils.reader import create_reader
1✔
21

22

23
PlaybackModeName = {
1✔
24
    PlaybackMode.one_loop: '单曲循环',
25
    PlaybackMode.sequential: '顺序播放',
26
    PlaybackMode.loop: '循环播放',
27
    PlaybackMode.random: '随机播放',
28
}
29
PlaybackModes = list(PlaybackModeName.keys())
1✔
30

31

32
def acolor(s, a):
1✔
33
    """Create color with it's name and alpha"""
34
    color = QColor(s)
1✔
35
    color.setAlpha(a)
1✔
36
    return color
1✔
37

38

39
class PlaylistOverlay(QWidget):
1✔
40
    def __init__(self, app, *args, **kwargs):
1✔
41
        super().__init__(*args, **kwargs)
1✔
42

43
        self._app = app
1✔
44
        self._tabbar = TabBar(self)
1✔
45
        self._clear_playlist_btn = TextButton('清空播放队列')
1✔
46
        self._playback_mode_switch = PlaybackModeSwitch(app)
1✔
47
        self._goto_current_song_btn = TextButton('跳转到当前歌曲')
1✔
48
        self._songs_radio_btn = TextButton('自动续歌')
1✔
49
        self._ai_radio_btn = TextButton('AI电台')
1✔
50
        # Please update the list when you add new buttons.
51
        self._btns = [
1✔
52
            self._clear_playlist_btn,
53
            self._playback_mode_switch,
54
            self._goto_current_song_btn,
55
            self._songs_radio_btn,
56
        ]
57
        self._stacked_layout = QStackedLayout()
1✔
58
        self._shadow_width = 15
1✔
59
        self._view_options = dict(row_height=60, no_scroll_v=False)
1✔
60
        self._player_playlist_view = PlayerPlaylistView(self._app, **self._view_options)
1✔
61

62
        # AutoFillBackground should be disabled for PlaylistOverlay so that shadow
63
        # effects can be simulated. AutoFillBackground should be enabled for tabbar.
64
        self._tabbar.setAutoFillBackground(True)
1✔
65

66
        self._clear_playlist_btn.clicked.connect(self._app.playlist.clear)
1✔
67
        self._goto_current_song_btn.clicked.connect(self.goto_current_song)
1✔
68
        self._songs_radio_btn.clicked.connect(self.enter_songs_radio)
1✔
69
        self._ai_radio_btn.clicked.connect(self.enter_ai_radio)
1✔
70
        esc_hide_widget(self)
1✔
71
        q_app = QApplication.instance()
1✔
72
        assert q_app is not None  # make type checker happy.
1✔
73
        # type ignore: q_app has focusChanged signal, but type checker can't find it.
74
        q_app.focusChanged.connect(self.on_focus_changed)  # type: ignore
1✔
75
        self._app.installEventFilter(self)
1✔
76
        self._tabbar.currentChanged.connect(self.show_tab)
1✔
77

78
        if (
1✔
79
            AI_RADIO_SUPPORTED is True
80
            and self._app.config.OPENAI_API_KEY
81
            and self._app.config.OPENAI_MODEL
82
            and self._app.config.OPENAI_API_BASEURL
83
        ):
NEW
84
            self._ai_radio_btn.clicked.connect(self.enter_ai_radio)
×
85
        else:
86
            self._ai_radio_btn.setDisabled(True)
1✔
87
        self.setup_ui()
1✔
88

89
    def setup_ui(self):
1✔
90
        self._layout = QVBoxLayout(self)
1✔
91
        self._btn_layout = QHBoxLayout()
1✔
92
        self._btn_layout2 = QHBoxLayout()
1✔
93
        self._layout.setContentsMargins(self._shadow_width, 0, 0, 0)
1✔
94
        self._layout.setSpacing(0)
1✔
95
        self._btn_layout.setContentsMargins(7, 7, 7, 7)
1✔
96
        self._btn_layout.setSpacing(7)
1✔
97
        self._btn_layout2.setContentsMargins(7, 0, 7, 7)
1✔
98
        self._btn_layout2.setSpacing(7)
1✔
99

100
        self._tabbar.setDocumentMode(True)
1✔
101
        self._tabbar.addTab('播放列表')
1✔
102
        self._tabbar.addTab('最近播放')
1✔
103
        self._layout.addWidget(self._tabbar)
1✔
104
        self._layout.addLayout(self._btn_layout)
1✔
105
        self._layout.addLayout(self._btn_layout2)
1✔
106
        self._layout.addLayout(self._stacked_layout)
1✔
107

108
        self._btn_layout.addWidget(self._clear_playlist_btn)
1✔
109
        self._btn_layout.addWidget(self._playback_mode_switch)
1✔
110
        self._btn_layout.addWidget(self._goto_current_song_btn)
1✔
111
        self._btn_layout2.addWidget(self._songs_radio_btn)
1✔
112
        self._btn_layout2.addWidget(self._ai_radio_btn)
1✔
113
        self._btn_layout.addStretch(0)
1✔
114
        self._btn_layout2.addStretch(0)
1✔
115

116
    def on_focus_changed(self, _, new):
1✔
117
        """
118
        Hide the widget when it loses focus.
119
        """
120
        if not self.isVisible():
1✔
121
            return
1✔
122
        # When the app is losing focus, the new is None.
123
        if new is None or new is self or new in self.findChildren(QWidget):
1✔
124
            return
1✔
125
        self.hide()
×
126

127
    def goto_current_song(self):
1✔
128
        view = self._stacked_layout.currentWidget()
×
129
        assert isinstance(view, PlayerPlaylistView)
×
130
        view.scroll_to_current_song()
×
131

132
    def enter_songs_radio(self):
1✔
133
        songs = self._app.playlist.list()
×
134
        if not songs:
×
135
            self._app.show_msg('播放队列为空,不能激活“自动续歌”功能')
×
136
        else:
137
            radio = SongsRadio(self._app, songs)
×
138
            self._app.fm.activate(radio.fetch_songs_func, reset=False)
×
139
            self._app.show_msg('“自动续歌”功能已激活')
×
140

141
    def enter_ai_radio(self):
1✔
NEW
142
        if self._app.playlist.list():
×
NEW
143
            radio = AIRadio(self._app)
×
NEW
144
            self._app.fm.activate(radio.fetch_songs_func, reset=False)
×
NEW
145
            self._app.show_msg('已经进入 AI 电台模式 ~')
×
146
        else:
NEW
147
            self._app.show_msg('播放列表为空,暂时不能开启 AI 电台')
×
148

149
    def show_tab(self, index):
1✔
150
        if not self.isVisible():
1✔
151
            return
1✔
152

153
        if index == 0:
1✔
154
            self._show_btns()
1✔
155
            view = self._player_playlist_view
1✔
156
        else:
157
            self._hide_btns()
×
158
            model = SongMiniCardListModel(
×
159
                create_reader(self._app.recently_played.list_songs()),
160
                fetch_cover_wrapper(self._app)
161
            )
162
            view = SongMiniCardListView(**self._view_options)
×
163
            view.setModel(model)
×
164
            view.play_song_needed.connect(self._app.playlist.play_model)
×
165
        delegate = SongMiniCardListDelegate(
1✔
166
            view,
167
            card_min_width=self.width() - self.width()//6,
168
            card_height=40,
169
            card_padding=(5 + SongMiniCardListDelegate.img_padding, 5, 0, 5),
170
            card_right_spacing=10,
171
        )
172
        view.setItemDelegate(delegate)
1✔
173
        self._stacked_layout.addWidget(view)
1✔
174
        self._stacked_layout.setCurrentWidget(view)
1✔
175

176
    def _hide_btns(self):
1✔
177
        for btn in self._btns:
×
178
            btn.hide()
×
179

180
    def _show_btns(self):
1✔
181
        for btn in self._btns:
1✔
182
            btn.show()
1✔
183

184
    def paintEvent(self, e):
1✔
185
        super().paintEvent(e)
1✔
186

187
        painter = QPainter(self)
1✔
188
        painter.setPen(Qt.NoPen)
1✔
189

190
        # Draw shadow effect on the left side.
191
        painter.save()
1✔
192
        shadow_width = self._shadow_width
1✔
193
        rect = QRect(0, 0, shadow_width, self.height())
1✔
194
        gradient = QLinearGradient(rect.topRight(), rect.topLeft())
1✔
195
        gradient.setColorAt(0, acolor('black', 70))
1✔
196
        gradient.setColorAt(0.05, acolor('black', 60))
1✔
197
        gradient.setColorAt(0.1, acolor('black', 30))
1✔
198
        gradient.setColorAt(0.2, acolor('black', 5))
1✔
199
        gradient.setColorAt(1, acolor('black', 0))
1✔
200
        painter.setBrush(gradient)
1✔
201
        painter.drawRect(rect)
1✔
202
        painter.restore()
1✔
203

204
        # Draw a rect to fill the remain background.
205
        painter.setBrush(self.palette().color(QPalette.Base))
1✔
206
        painter.drawRect(shadow_width, 0, self.width()-shadow_width, self.height())
1✔
207

208
    def showEvent(self, e):
1✔
209
        super().showEvent(e)
1✔
210
        self.show_tab(self._tabbar.currentIndex())
1✔
211

212
    def eventFilter(self, obj, event):
1✔
213
        """
214
        Hide myself when the app is resized.
215
        """
216
        if obj is self._app and event.type() == QEvent.Resize:
1✔
217
            self.hide()
×
218
        return False
1✔
219

220

221
class PlaybackModeSwitch(TextButton):
1✔
222
    def __init__(self, app, *args, **kwargs):
1✔
223
        super().__init__(*args, **kwargs)
1✔
224
        self._app = app
1✔
225

226
        self.update_text()
1✔
227
        self.clicked.connect(self.switch_playback_mode)
1✔
228
        self._app.playlist.playback_mode_changed.connect(
1✔
229
            self.on_playback_mode_changed, aioqueue=True)
230
        self.setToolTip('修改播放模式')
1✔
231

232
    def switch_playback_mode(self):
1✔
233
        playlist = self._app.playlist
×
234
        index = PlaybackModes.index(playlist.playback_mode)
×
235
        if index < len(PlaybackModes) - 1:
×
236
            new_index = index + 1
×
237
        else:
238
            new_index = 0
×
239
        playlist.playback_mode = PlaybackModes[new_index]
×
240

241
    def update_text(self):
1✔
242
        self.setText(PlaybackModeName[self._app.playlist.playback_mode])
1✔
243

244
    def on_playback_mode_changed(self, _):
1✔
245
        self.update_text()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc