Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gui: add menu action '搜索相似资源' #876

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 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 @@ -55,6 +58,8 @@ async def goto_song_album(song):
self._app.show_msg('该歌曲没有专辑信息')

menu.hovered.connect(self.on_action_hovered)
menu.addAction('搜索相似资源').triggered.connect(
lambda: self.show_similar_resource(song))
artist_menu = menu.addMenu('查看歌手')
artist_menu.menuAction().setData({'artists': None, 'song': song})
mv_menu = menu.addMenu(MV_BTN_TEXT)
Expand All @@ -67,6 +72,34 @@ async def goto_song_album(song):
menu.addAction('歌曲详情').triggered.connect(
lambda: goto_song_explore(song))

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

class SearchResultViewWithEsc(SearchResultView):
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
else:
super().keyPressEvent(event)

q = f'{song.title} {song.artists_name}'
view = SearchResultViewWithEsc(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):
"""
Fetch song.artists when artists_action is hovered. If it is
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)
Loading
Loading