Skip to content

Commit

Permalink
gui: add menu action '搜索相似资源'
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven committed Nov 10, 2024
1 parent 791bb9a commit e854e5a
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 226 deletions.
28 changes: 27 additions & 1 deletion feeluown/gui/components/menu.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from typing import Optional, TYPE_CHECKING

from PyQt5.QtCore import Qt, QPoint

from feeluown.excs import ProviderIOError
from feeluown.utils.aio import run_fn, run_afn
from feeluown.player import SongRadio
from feeluown.library import SongModel, VideoModel
from feeluown.library import SongModel, VideoModel, SearchType
from feeluown.gui.widgets.magicbox import KeySourceIn

if TYPE_CHECKING:
from feeluown.app.gui_app import GuiApp
Expand Down Expand Up @@ -66,6 +69,29 @@ async def goto_song_album(song):
lambda: enter_song_radio(song))
menu.addAction('歌曲详情').triggered.connect(
lambda: goto_song_explore(song))
menu.addAction('搜索相似资源').triggered.connect(
lambda: self.show_similar_resource(song))

def show_similar_resource(self, song):
from feeluown.gui.components.search import SearchResultView

q = f'{song.title} {song.artists_name}'
view = SearchResultView(self._app, parent=self._app)
source_in = self._app.browser.local_storage.get(KeySourceIn, None)
run_afn(view.search_and_render, q, SearchType.so, source_in)

width = self._app.width() - self._app.ui.sidebar.width()
height = self._app.height() * 3 // 5
x = self._app.ui.sidebar.width()
y = self._app.height() - height - self._app.ui.player_bar.height()

# Set the size using resize() and position using move()
view.resize(width, height)
pos = self._app.mapToGlobal(QPoint(0, 0))
view.move(pos.x() + x, pos.y() + y)
view.setWindowFlags(Qt.Popup)
view.show()
view.raise_()

def on_action_hovered(self, action):
"""
Expand Down
153 changes: 153 additions & 0 deletions feeluown/gui/components/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from datetime import datetime

from PyQt5.QtWidgets import QAbstractItemView, QFrame, QVBoxLayout

from feeluown.library import SearchType
from feeluown.gui.page_containers.table import TableContainer, Renderer
from feeluown.gui.page_containers.scroll_area import ScrollArea
from feeluown.gui.widgets.img_card_list import ImgCardListDelegate
from feeluown.gui.widgets.songs import SongsTableView, ColumnsMode
from feeluown.gui.base_renderer import TabBarRendererMixin
from feeluown.gui.helpers import BgTransparentMixin
from feeluown.gui.widgets.magicbox import KeySourceIn, KeyType
from feeluown.gui.widgets.header import LargeHeader, MidHeader
from feeluown.gui.widgets.accordion import Accordion
from feeluown.gui.widgets.labels import MessageLabel
from feeluown.utils.reader import create_reader


Tabs = [('歌曲', SearchType.so),
('专辑', SearchType.al),
('歌手', SearchType.ar),
('歌单', SearchType.pl),
('视频', SearchType.vi)]


def get_tab_idx(search_type):
for i, tab in enumerate(Tabs):
if tab[1] == search_type:
return i
raise ValueError("unknown search type")


class SearchResultView(ScrollArea):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)

self.body = Body(app)
self.setWidget(self.body)

def fillable_bg_height(self):
"""Implement VFillableBg protocol"""
return self.body.height() - self.body.accordion.height()

async def search_and_render(self, *args, **kwargs):
await self.body.search_and_render(*args, **kwargs)


class Body(QFrame, BgTransparentMixin):
"""
view = SearchResultView(app, q)
await view.render()
"""
def __init__(self, app, **kwargs):
super().__init__(**kwargs)

self._app = app

self.title = LargeHeader()
self.hint = MessageLabel()
self.accordion = Accordion()

self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(20, 0, 20, 0)
self._layout.setSpacing(0)
self._layout.addSpacing(30)
self._layout.addWidget(self.title)
self._layout.addSpacing(10)
self._layout.addWidget(self.hint)
self._layout.addSpacing(10)
self._layout.addWidget(self.accordion)
self._layout.addStretch(0)

async def search_and_render(self, q, search_type, source_in):
# pylint: disable=too-many-locals
view = self
app = self._app

self.title.setText(f'搜索“{q}”')

tab_index = get_tab_idx(search_type)
succeed = 0
start = datetime.now()
is_first = True # Is first search result.
view.hint.show_msg('正在搜索...')
async for result in app.library.a_search(
q, type_in=search_type, source_in=source_in):
table_container = TableContainer(app, view.accordion)
table_container.layout().setContentsMargins(0, 0, 0, 0)

# HACK: set fixed row for tables.
# pylint: disable=protected-access
for table in table_container._tables:
assert isinstance(table, QAbstractItemView)
delegate = table.itemDelegate()
if isinstance(delegate, ImgCardListDelegate):
# FIXME: set fixed_row_count in better way.
table._fixed_row_count = 2 # type: ignore[attr-defined]
delegate.update_settings("card_min_width", 140)
elif isinstance(table, SongsTableView):
table._fixed_row_count = 8
table._row_height = table.verticalHeader().defaultSectionSize()

renderer = SearchResultRenderer(q, tab_index, source_in=source_in)
await table_container.set_renderer(renderer)
_, search_type, attrname, show_handler = renderer.tabs[tab_index]
objects = getattr(result, attrname) or []
if not objects: # Result is empty.
continue

succeed += 1
if search_type is SearchType.so:
show_handler( # type: ignore[operator]
create_reader(objects), columns_mode=ColumnsMode.playlist)
else:
show_handler(create_reader(objects)) # type: ignore[operator]
source = objects[0].source
provider = app.library.get(source)
provider_name = provider.name
if is_first is False:
table_container.hide()
view.accordion.add_section(MidHeader(provider_name), table_container, 6, 12)
renderer.meta_widget.hide()
renderer.toolbar.hide()
is_first = False
time_cost = (datetime.now() - start).total_seconds()
view.hint.show_msg(f'搜索完成,共有 {succeed} 个有效的结果,花费 {time_cost:.2f}s')


class SearchResultRenderer(Renderer, TabBarRendererMixin):
def __init__(self, q, tab_index, source_in=None):
self.q = q
self.tab_index = tab_index
self.source_in = source_in

self.tabs = [
(*Tabs[0], 'songs', self.show_songs),
(*Tabs[1], 'albums', self.show_albums),
(*Tabs[2], 'artists', self.show_artists),
(*Tabs[3], 'playlists', self.show_playlists),
(*Tabs[4], 'videos', self.show_videos),
]

async def render(self):
self.render_tab_bar()

def render_by_tab_index(self, tab_index):
search_type = self.tabs[tab_index][1]
self._app.browser.local_storage[KeyType] = search_type.value
query = {'q': self.q, 'type': search_type.value}
source_in = self._app.browser.local_storage.get(KeySourceIn, None)
if source_in is not None:
query['source_in'] = source_in
self._app.browser.goto(page='/search', query=query)
140 changes: 4 additions & 136 deletions feeluown/gui/pages/search.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
from datetime import datetime
from PyQt5.QtWidgets import QAbstractItemView, QFrame, QVBoxLayout

from feeluown.library import SearchType
from feeluown.gui.page_containers.table import TableContainer, Renderer
from feeluown.gui.page_containers.scroll_area import ScrollArea
from feeluown.gui.widgets.img_card_list import ImgCardListDelegate
from feeluown.gui.widgets.songs import SongsTableView, ColumnsMode
from feeluown.gui.base_renderer import TabBarRendererMixin
from feeluown.gui.helpers import BgTransparentMixin
from feeluown.gui.widgets.magicbox import KeySourceIn, KeyType
from feeluown.gui.widgets.header import LargeHeader, MidHeader
from feeluown.gui.widgets.accordion import Accordion
from feeluown.gui.widgets.labels import MessageLabel
from feeluown.utils.reader import create_reader
from feeluown.utils.router import Request
from feeluown.app.gui_app import GuiApp

Tabs = [('歌曲', SearchType.so),
('专辑', SearchType.al),
('歌手', SearchType.ar),
('歌单', SearchType.pl),
('视频', SearchType.vi)]


def get_tab_idx(search_type):
for i, tab in enumerate(Tabs):
if tab[1] == search_type:
return i
raise ValueError("unknown search type")
from feeluown.gui.components.search import SearchResultView


def get_source_in(req: Request):
Expand All @@ -53,112 +27,6 @@ async def render(req: Request, **kwargs):
source_in = get_source_in(req)
search_type = SearchType(req.query.get('type', SearchType.so.value))

body = Body()
view = View(app, q)
body.setWidget(view)
app.ui.right_panel.set_body(body)

tab_index = get_tab_idx(search_type)
succeed = 0
start = datetime.now()
is_first = True # Is first search result.
view.hint.show_msg('正在搜索...')
async for result in app.library.a_search(
q, type_in=search_type, source_in=source_in):
table_container = TableContainer(app, view.accordion)
table_container.layout().setContentsMargins(0, 0, 0, 0)

# HACK: set fixed row for tables.
# pylint: disable=protected-access
for table in table_container._tables:
assert isinstance(table, QAbstractItemView)
delegate = table.itemDelegate()
if isinstance(delegate, ImgCardListDelegate):
# FIXME: set fixed_row_count in better way.
table._fixed_row_count = 2 # type: ignore[attr-defined]
delegate.update_settings("card_min_width", 140)
elif isinstance(table, SongsTableView):
table._fixed_row_count = 8
table._row_height = table.verticalHeader().defaultSectionSize()

renderer = SearchResultRenderer(q, tab_index, source_in=source_in)
await table_container.set_renderer(renderer)
_, search_type, attrname, show_handler = renderer.tabs[tab_index]
objects = getattr(result, attrname) or []
if not objects: # Result is empty.
continue

succeed += 1
if search_type is SearchType.so:
show_handler( # type: ignore[operator]
create_reader(objects), columns_mode=ColumnsMode.playlist)
else:
show_handler(create_reader(objects)) # type: ignore[operator]
source = objects[0].source
provider = app.library.get(source)
provider_name = provider.name
if is_first is False:
table_container.hide()
view.accordion.add_section(MidHeader(provider_name), table_container, 6, 12)
renderer.meta_widget.hide()
renderer.toolbar.hide()
is_first = False
time_cost = (datetime.now() - start).total_seconds()
view.hint.show_msg(f'搜索完成,共有 {succeed} 个有效的结果,花费 {time_cost:.2f}s')


class SearchResultRenderer(Renderer, TabBarRendererMixin):
def __init__(self, q, tab_index, source_in=None):
self.q = q
self.tab_index = tab_index
self.source_in = source_in

self.tabs = [
(*Tabs[0], 'songs', self.show_songs),
(*Tabs[1], 'albums', self.show_albums),
(*Tabs[2], 'artists', self.show_artists),
(*Tabs[3], 'playlists', self.show_playlists),
(*Tabs[4], 'videos', self.show_videos),
]

async def render(self):
self.render_tab_bar()

def render_by_tab_index(self, tab_index):
search_type = self.tabs[tab_index][1]
self._app.browser.local_storage[KeyType] = search_type.value
query = {'q': self.q, 'type': search_type.value}
source_in = self._app.browser.local_storage.get(KeySourceIn, None)
if source_in is not None:
query['source_in'] = source_in
self._app.browser.goto(page='/search', query=query)


class Body(ScrollArea):
def fillable_bg_height(self):
"""Implement VFillableBg protocol"""
view = self.widget()
assert isinstance(view, View) # make type chckign happy.
return view.height() - view.accordion.height()


class View(QFrame, BgTransparentMixin):
def __init__(self, app, q):
super().__init__()

self._app = app

self.title = LargeHeader(f'搜索“{q}”')
self.hint = MessageLabel()
self.accordion = Accordion()

self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(20, 0, 20, 0)
self._layout.setSpacing(0)
self._layout.addSpacing(30)
self._layout.addWidget(self.title)
self._layout.addSpacing(10)
self._layout.addWidget(self.hint)
self._layout.addSpacing(10)
self._layout.addWidget(self.accordion)
self._layout.addStretch(0)
view = SearchResultView(app)
app.ui.right_panel.set_body(view)
await view.search_and_render(q, search_type, source_in)
15 changes: 0 additions & 15 deletions feeluown/gui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def __init__(self, app):
self.forward_btn = self.bottom_panel.forward_btn

self.toolbar.settings_btn.clicked.connect(self._open_settings_dialog)
self.songs_table.about_to_show_menu.connect(self._open_standby_list_overlay)

self._setup_ui()

Expand Down Expand Up @@ -86,20 +85,6 @@ def _open_settings_dialog(self):
dialog = SettingsDialog(self._app, self._app)
dialog.exec()

def _open_standby_list_overlay(self, ctx):
from feeluown.gui.uimain.standby import StandbyListOverlay

models = ctx['models']
add_action = ctx['add_action']
song = models[0]

def callback(_):
overlay = StandbyListOverlay(self._app)
overlay.show_song(song)
overlay.exec()

add_action('寻找可播放资源', callback)

def toggle_player_bar(self):
if self.top_panel.isVisible():
self.top_panel.hide()
Expand Down
Loading

0 comments on commit e854e5a

Please sign in to comment.