diff --git a/feeluown/app/gui_app.py b/feeluown/app/gui_app.py index 01a2d58d22..188ba05cf6 100644 --- a/feeluown/app/gui_app.py +++ b/feeluown/app/gui_app.py @@ -115,11 +115,18 @@ def apply_state(self, state): gui = state.get('gui', {}) lyric = gui.get('lyric', {}) + local_storage = gui.get('browser', {}).get('local_storage', {}) + self.browser.local_storage = local_storage self.ui.lyric_window.apply_state(lyric) def dump_state(self): state = super().dump_state() - state['gui'] = {'lyric': self.ui.lyric_window.dump_state()} + state['gui'] = { + 'lyric': self.ui.lyric_window.dump_state(), + 'browser': { + 'local_storage': self.browser.local_storage + } + } return state def closeEvent(self, _): diff --git a/feeluown/entry_points/run_app.py b/feeluown/entry_points/run_app.py index dac915ebf8..5475a96f4a 100644 --- a/feeluown/entry_points/run_app.py +++ b/feeluown/entry_points/run_app.py @@ -133,6 +133,7 @@ def shutdown(_): # GUI state must be load before running app, otherwise, it does not take effects. app.load_and_apply_state() + logger.info("Load app last state...") # App can exit in several ways. # @@ -146,6 +147,7 @@ def shutdown(_): # 1. Ctrl-C # 2. SIGTERM app.run() + logger.info("App started") app.started.emit(app) await sentinal diff --git a/feeluown/gui/helpers.py b/feeluown/gui/helpers.py index 7e029a77b4..df56e8af0d 100644 --- a/feeluown/gui/helpers.py +++ b/feeluown/gui/helpers.py @@ -621,6 +621,7 @@ def esc_hide_widget(widget): # https://ethanschoonover.com/solarized/ +# Do not change the existing colors if they are used by some widgets/components. SOLARIZED_COLORS = { 'yellow': '#b58900', 'orange': '#cb4b16', diff --git a/feeluown/gui/pages/search.py b/feeluown/gui/pages/search.py index d7ab9ba00a..2444ab2e51 100644 --- a/feeluown/gui/pages/search.py +++ b/feeluown/gui/pages/search.py @@ -1,3 +1,4 @@ +from datetime import datetime from PyQt5.QtWidgets import QAbstractItemView, QFrame, QVBoxLayout from feeluown.library import SearchType @@ -10,7 +11,10 @@ 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), @@ -19,76 +23,88 @@ ('视频', SearchType.vi)] -async def render(req, **kwargs): # pylint: disable=too-many-locals,too-many-branches +def get_tab_idx(search_type): + for i, tab in enumerate(Tabs): + if tab[1] == search_type: + return i + raise ValueError("unknown search type") + + +def get_source_in(req: Request): + source_in = req.query.get('source_in', None) + if source_in is not None: + source_in = source_in.split(',') + else: + source_in = None + return source_in + + +async def render(req: Request, **kwargs): """/search handler :type app: feeluown.app.App """ + # pylint: disable=too-many-locals,too-many-branches q = req.query.get('q', '') if not q: return - source_in = req.query.get('source_in', None) + app: 'GuiApp' = req.ctx['app'] + source_in = get_source_in(req) search_type = SearchType(req.query.get('type', SearchType.so.value)) - if source_in is not None: - source_in = source_in.split(',') - else: - source_in = None - - app = req.ctx['app'] body = Body() view = View(app, q) body.setWidget(view) app.ui.right_panel.set_body(body) - tab_index = 0 - for i, tab in enumerate(Tabs): - if tab[1] == search_type: - tab_index = i - break - + 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): - if result is not None: - 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", 100) - 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 - - 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 + 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", 100) + 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): @@ -133,6 +149,7 @@ def __init__(self, app, q): self._app = app self.title = LargeHeader(f'搜索“{q}”') + self.hint = MessageLabel() self.accordion = Accordion() self._layout = QVBoxLayout(self) @@ -141,5 +158,7 @@ def __init__(self, app, q): 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) diff --git a/feeluown/gui/pages/template.py b/feeluown/gui/pages/template.py index b01e003916..5b1cf3d0fc 100644 --- a/feeluown/gui/pages/template.py +++ b/feeluown/gui/pages/template.py @@ -1,14 +1,14 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QLabel + +from feeluown.gui.widgets.labels import MessageLabel if TYPE_CHECKING: from feeluown.app.gui_app import GuiApp async def render_error_message(app: 'GuiApp', msg: str): - label = QLabel(f"错误提示:{msg}") - label.setTextFormat(Qt.RichText) + label = MessageLabel(msg, MessageLabel.ERROR) label.setAlignment(Qt.AlignCenter) app.ui.page_view.set_body(label) diff --git a/feeluown/gui/widgets/labels.py b/feeluown/gui/widgets/labels.py index 4cd23e4e72..8c8da6203d 100644 --- a/feeluown/gui/widgets/labels.py +++ b/feeluown/gui/widgets/labels.py @@ -2,7 +2,7 @@ from PyQt5.QtWidgets import QLabel, QSizePolicy from feeluown.utils.utils import parse_ms -from feeluown.gui.helpers import elided_text +from feeluown.gui.helpers import elided_text, SOLARIZED_COLORS def format_second(s): @@ -72,3 +72,28 @@ def __init__(self, app, parent=None): def on_position_changed(self, position): self.setText(format_second(position or 0)) + + +class MessageLabel(QLabel): + """Show warning/error message. + """ + INFO = 'info' + ERROR = 'error' + + def __init__(self, text='', level=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setTextFormat(Qt.RichText) + self.show_msg(text, level) + + def show_msg(self, text, level=None): + if level == MessageLabel.ERROR: + hint = '错误提示:' + color = 'red' + elif level == MessageLabel.INFO: + hint = '️提示:' + color = SOLARIZED_COLORS['blue'] + else: + hint = '️' + color = SOLARIZED_COLORS['blue'] + self.setText(f"{hint}{text}")