Example #1
0
    def __init__(self, app, parent=None):
        super().__init__(parent)

        self._app = app
        self._renderer = None
        self._table = None  # current visible table
        self._tables = []

        self.toolbar = SongsTableToolbar()
        self.tabbar = TableTabBarV2()
        self.meta_widget = TableMetaWidget(parent=self)
        self.songs_table = SongsTableView(parent=self)
        self.albums_table = AlbumListView(parent=self)
        self.desc_widget = DescLabel(parent=self)

        self._tables.append(self.songs_table)
        self._tables.append(self.albums_table)

        self.songs_table.play_song_needed.connect(
            lambda song: asyncio.ensure_future(self.play_song(song)))
        self.songs_table.show_artist_needed.connect(
            lambda artist: self._app.browser.goto(model=artist))
        self.songs_table.show_album_needed.connect(
            lambda album: self._app.browser.goto(model=album))

        self.toolbar.play_all_needed.connect(self.play_all)
        self.songs_table.add_to_playlist_needed.connect(
            self._add_songs_to_playlist)

        self._setup_ui()
Example #2
0
    def __init__(self, app, parent=None):
        super().__init__(parent)

        self._app = app
        self._renderer = None
        self._table = None  # current visible table
        self._tables = []

        self._extra = None
        self.toolbar = SongsTableToolbar()
        self.tabbar = TableTabBarV2()
        self.meta_widget = TableMetaWidget(parent=self)
        self.songs_table = SongsTableView(parent=self)
        self.albums_table = AlbumListView(parent=self)
        self.artists_table = ArtistListView(parent=self)
        self.videos_table = VideoListView(parent=self)
        self.playlists_table = PlaylistListView(parent=self)
        self.comments_table = CommentListView(parent=self)
        self.desc_widget = DescLabel(parent=self)

        self._tables.append(self.songs_table)
        self._tables.append(self.albums_table)
        self._tables.append(self.artists_table)
        self._tables.append(self.playlists_table)
        self._tables.append(self.videos_table)
        self._tables.append(self.comments_table)

        self.songs_table.play_song_needed.connect(
            lambda song: asyncio.ensure_future(self.play_song(song)))
        self.videos_table.play_video_needed.connect(
            lambda video: aio.create_task(self.play_video(video)))

        def goto_model(model):
            self._app.browser.goto(model=model)

        for signal in [
                self.songs_table.show_artist_needed,
                self.songs_table.show_album_needed,
                self.albums_table.show_album_needed,
                self.artists_table.show_artist_needed,
                self.playlists_table.show_playlist_needed,
        ]:
            signal.connect(goto_model)

        self.toolbar.play_all_needed.connect(self.play_all)
        self.songs_table.add_to_playlist_needed.connect(
            self._add_songs_to_playlist)
        self.songs_table.about_to_show_menu.connect(
            self._songs_table_about_to_show_menu)
        self.songs_table.activated.connect(lambda index: aio.create_task(
            self._on_songs_table_activated(index)))

        self._setup_ui()
Example #3
0
def test_table_meta(qtbot):
    widget = TableMetaWidget(SongsTableToolbar())
    qtbot.addWidget(widget)
    widget.title = '我喜欢的音乐'
    widget.subtitle = '嘿嘿'
    widget.creator = 'cosven'
    widget.updated_at = time.time()
    widget.desc = "<pre><code>print('hello world')</code><pre>"
Example #4
0
    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.title_label = QLabel(self)
        self.cover_label = QLabel(self)
        self.meta_label = QLabel(self)
        self.desc_container = DescriptionContainer(parent=self)
        self.toolbar = SongsTableToolbar(parent=self)

        self.title_label.setTextFormat(Qt.RichText)
        self.meta_label.setTextFormat(Qt.RichText)

        self._is_fullwindow = False

        self._setup_ui()
        self._refresh()

        self.desc_container.space_pressed.connect(self.toggle_full_window)
Example #5
0
    def __init__(self, app, parent=None):
        super().__init__(parent)
        self._app = app

        self.toolbar = SongsTableToolbar()
        self.meta_widget = TableMetaWidget(self.toolbar, parent=self)
        self.songs_table = SongsTableView(parent=self)
        self.albums_table = AlbumListView(parent=self)

        self.songs_table.play_song_needed.connect(
            lambda song: asyncio.ensure_future(self.play_song(song)))
        self.songs_table.show_artist_needed.connect(
            lambda artist: self._app.browser.goto(model=artist))
        self.songs_table.show_album_needed.connect(
            lambda album: self._app.browser.goto(model=album))

        self.meta_widget.toolbar.play_all_needed.connect(self.play_all)
        self.meta_widget.toggle_full_window_needed.connect(
            self.toggle_meta_full_window)

        self.hide()
        self._setup_ui()

        self._delegate = None
Example #6
0
class TableContainer(QFrame, BgTransparentMixin):
    def __init__(self, app, parent=None):
        super().__init__(parent)

        self._app = app
        self._renderer = None
        self._table = None  # current visible table
        self._tables = []

        self._extra = None
        self.toolbar = SongsTableToolbar()
        self.tabbar = TableTabBarV2()
        self.meta_widget = TableMetaWidget(parent=self)
        self.songs_table = SongsTableView(parent=self)
        self.albums_table = AlbumListView(parent=self)
        self.artists_table = ArtistListView(parent=self)
        self.videos_table = VideoListView(parent=self)
        self.playlists_table = PlaylistListView(parent=self)
        self.desc_widget = DescLabel(parent=self)

        self._tables.append(self.songs_table)
        self._tables.append(self.albums_table)
        self._tables.append(self.artists_table)
        self._tables.append(self.playlists_table)
        self._tables.append(self.videos_table)

        self.songs_table.play_song_needed.connect(
            lambda song: asyncio.ensure_future(self.play_song(song)))
        self.videos_table.play_video_needed.connect(
            lambda video: aio.create_task(self.play_video(video)))

        def goto_model(model):
            self._app.browser.goto(model=model)

        for signal in [
                self.songs_table.show_artist_needed,
                self.songs_table.show_album_needed,
                self.albums_table.show_album_needed,
                self.artists_table.show_artist_needed,
                self.playlists_table.show_playlist_needed,
        ]:
            signal.connect(goto_model)

        self.toolbar.play_all_needed.connect(self.play_all)
        self.songs_table.add_to_playlist_needed.connect(
            self._add_songs_to_playlist)

        self._setup_ui()

    def _setup_ui(self):
        self.current_table = None
        self.tabbar.hide()
        self.meta_widget.add_tabbar(self.tabbar)
        self.desc_widget.hide()

        self._layout = QVBoxLayout(self)
        self._layout.addWidget(self.meta_widget)
        self._layout.addWidget(self.toolbar)
        self._layout.addSpacing(10)
        self._layout.addWidget(self.desc_widget)
        for table in self._tables:
            self._layout.addWidget(table)
        self._layout.addStretch(0)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.setSpacing(0)

    @property
    def current_extra(self):
        return self._extra

    @current_extra.setter
    def current_extra(self, extra):
        """(alpha)"""
        if self._extra is not None:
            self._layout.removeWidget(self._extra)
            self._extra.deleteLater()
            del self._extra
        self._extra = extra
        if self._extra is not None:
            self._layout.insertWidget(1, self._extra)

    @property
    def current_table(self):
        """current visible table, if no table is visible, return None"""
        return self._table

    @current_table.setter
    def current_table(self, table):
        """set table as current visible table

        show table and hide other tables, if table is None,
        hide all tables.
        """
        for t in self._tables:
            if t != table:
                t.hide()
        if table is None:
            self.toolbar.hide()
        else:
            self.desc_widget.hide()
            table.show()
            if table is self.artists_table:
                self.toolbar.artists_mode()
            if table is self.albums_table:
                self.toolbar.albums_mode()
            if table is self.songs_table:
                self.toolbar.songs_mode()
        self._table = table

    async def set_renderer(self, renderer):
        """set ui renderer

        TODO: add lock for set_renderer
        """

        if renderer is None:
            return

        # firstly, tear down everything
        # tear down last renderer
        if self._renderer is not None:
            await self._renderer.tearDown()
        self.meta_widget.hide()
        self.meta_widget.clear()
        self.tabbar.hide()
        self.tabbar.check_default()
        self.current_table = None
        self.current_extra = None
        # clean right_panel background image
        self._app.ui.right_panel.show_background_image(None)
        # disconnect songs_table signal
        signals = (
            self.tabbar.show_contributed_albums_needed,
            self.tabbar.show_albums_needed,
            self.tabbar.show_songs_needed,
            self.tabbar.show_artists_needed,
            self.tabbar.show_playlists_needed,
            self.tabbar.show_desc_needed,
        )
        for signal in signals:
            disconnect_slots_if_has(signal)

        # unbind some callback function
        self.songs_table.remove_song_func = None

        # secondly, prepare environment
        self.show()

        # thirdly, setup new renderer
        await renderer.setUp(self)
        self._renderer = renderer
        await self._renderer.render()

    async def play_song(self, song):
        self._app.player.play_song(song)

    async def play_video(self, video):
        media = await aio.run_in_executor(None, lambda: video.media)
        self._app.player.play(media)

    def play_all(self):
        task_name = 'play-all'
        task_spec = self._app.task_mgr.get_or_create(task_name)

        def reader_readall_cb(task):
            with suppress(ProviderIOError, asyncio.CancelledError):
                songs = task.result()
                self._app.player.play_songs(songs=songs)
            self.toolbar.enter_state_playall_end()

        model = self.songs_table.model()
        # FIXME: think about a more elegant way
        reader = model.sourceModel().songs_g
        if reader is not None:
            if reader.count is not None:
                task = task_spec.bind_blocking_io(reader.readall)
                self.toolbar.enter_state_playall_start()
                task.add_done_callback(reader_readall_cb)
                return
        songs = model.sourceModel().songs
        self._app.player.play_songs(songs=songs)

    async def show_model(self, model):
        model_type = ModelType(model.meta.model_type)
        if model_type == ModelType.album:
            renderer = AlbumRenderer(model)
        elif model_type == ModelType.artist:
            renderer = ArtistRenderer(model)
        elif model_type == ModelType.playlist:
            renderer = PlaylistRenderer(model)
        else:
            renderer = None
        await self.set_renderer(renderer)

    def show_collection(self, coll):
        renderer = SongsCollectionRenderer(coll)
        aio.create_task(self.set_renderer(renderer))

    def show_songs(self, songs=None, songs_g=None):
        """(DEPRECATED) provided only for backward compatibility"""
        renderer = Renderer()
        task = aio.create_task(self.set_renderer(renderer))
        task.add_done_callback(
            lambda _: renderer.show_songs(songs=songs, songs_g=songs_g))

    def show_albums_coll(self, albums_g):
        aio.create_task(self.set_renderer(AlbumsCollectionRenderer(albums_g)))

    def show_artists_coll(self, artists_g):
        aio.create_task(self.set_renderer(
            ArtistsCollectionRenderer(artists_g)))

    def show_player_playlist(self):
        aio.create_task(self.set_renderer(PlayerPlaylistRenderer()))

    def search(self, text):
        if self.isVisible() and self.songs_table is not None:
            self.songs_table.filter_row(text)

    def _add_songs_to_playlist(self, songs):
        for song in songs:
            self._app.playlist.add(song)
Example #7
0
class TableContainer(QFrame, BgTransparentMixin):
    def __init__(self, app, parent=None):
        super().__init__(parent)

        self._app = app
        self._renderer = None
        self._table = None  # current visible table
        self._tables = []

        self._extra = None
        self.toolbar = SongsTableToolbar()
        self.tabbar = TableTabBarV2()
        self.meta_widget = TableMetaWidget(parent=self)
        self.songs_table = SongsTableView(parent=self)
        self.albums_table = AlbumListView(parent=self)
        self.artists_table = ArtistListView(parent=self)
        self.videos_table = VideoListView(parent=self)
        self.playlists_table = PlaylistListView(parent=self)
        self.comments_table = CommentListView(parent=self)
        self.desc_widget = DescLabel(parent=self)

        self._tables.append(self.songs_table)
        self._tables.append(self.albums_table)
        self._tables.append(self.artists_table)
        self._tables.append(self.playlists_table)
        self._tables.append(self.videos_table)
        self._tables.append(self.comments_table)

        self.songs_table.play_song_needed.connect(
            lambda song: asyncio.ensure_future(self.play_song(song)))
        self.videos_table.play_video_needed.connect(
            lambda video: aio.create_task(self.play_video(video)))

        def goto_model(model):
            self._app.browser.goto(model=model)

        for signal in [
                self.songs_table.show_artist_needed,
                self.songs_table.show_album_needed,
                self.albums_table.show_album_needed,
                self.artists_table.show_artist_needed,
                self.playlists_table.show_playlist_needed,
        ]:
            signal.connect(goto_model)

        self.toolbar.play_all_needed.connect(self.play_all)
        self.songs_table.add_to_playlist_needed.connect(
            self._add_songs_to_playlist)
        self.songs_table.about_to_show_menu.connect(
            self._songs_table_about_to_show_menu)
        self.songs_table.activated.connect(lambda index: aio.create_task(
            self._on_songs_table_activated(index)))

        self._setup_ui()

    def _setup_ui(self):
        self.current_table = None
        self.tabbar.hide()
        self.meta_widget.add_tabbar(self.tabbar)
        self.desc_widget.hide()

        self._layout = QVBoxLayout(self)
        self._layout.addWidget(self.meta_widget)
        self._layout.addWidget(self.toolbar)
        self._layout.addSpacing(10)
        self._layout.addWidget(self.desc_widget)
        for table in self._tables:
            self._layout.addWidget(table)
        self._layout.addStretch(0)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.setSpacing(0)

    @property
    def current_extra(self):
        return self._extra

    @current_extra.setter
    def current_extra(self, extra):
        """(alpha)"""
        if self._extra is not None:
            self._layout.removeWidget(self._extra)
            self._extra.deleteLater()
            del self._extra
        self._extra = extra
        if self._extra is not None:
            self._layout.insertWidget(1, self._extra)

    @property
    def current_table(self):
        """current visible table, if no table is visible, return None"""
        return self._table

    @current_table.setter
    def current_table(self, table):
        """set table as current visible table

        show table and hide other tables, if table is None,
        hide all tables.
        """
        for t in self._tables:
            if t != table:
                t.hide()
        if table is None:
            self.toolbar.hide()
        else:
            self.desc_widget.hide()
            table.show()
            if table is self.artists_table:
                self.toolbar.artists_mode()
            if table is self.albums_table:
                self.toolbar.albums_mode()
            if table is self.songs_table:
                self.toolbar.songs_mode()
        self._table = table

    async def set_renderer(self, renderer):
        """set ui renderer

        TODO: add lock for set_renderer
        """

        if renderer is None:
            return

        # firstly, tear down everything
        # tear down last renderer
        if self._renderer is not None:
            await self._renderer.tearDown()
        self.meta_widget.hide()
        self.meta_widget.clear()
        self.tabbar.hide()
        self.tabbar.check_default()
        self.current_table = None
        self.current_extra = None
        # clean right_panel background image
        self._app.ui.right_panel.show_background_image(None)
        # disconnect songs_table signal
        signals = (
            self.tabbar.show_contributed_albums_needed,
            self.tabbar.show_albums_needed,
            self.tabbar.show_songs_needed,
            self.tabbar.show_artists_needed,
            self.tabbar.show_playlists_needed,
            self.tabbar.show_desc_needed,
        )
        for signal in signals:
            disconnect_slots_if_has(signal)

        # unbind some callback function
        self.songs_table.remove_song_func = None

        # secondly, prepare environment
        self.show()

        # thirdly, setup new renderer
        await renderer.setUp(self)
        self._renderer = renderer
        await self._renderer.render()

    async def play_song(self, song):
        self._app.player.play_song(song)

    async def play_video(self, video):
        media = await aio.run_in_executor(None, lambda: video.media)
        self._app.player.play(media)

    def play_all(self):
        task_name = 'play-all'
        task_spec = self._app.task_mgr.get_or_create(task_name)

        def reader_readall_cb(task):
            with suppress(ProviderIOError, asyncio.CancelledError):
                songs = task.result()
                self._app.player.play_songs(songs=songs)
            self.toolbar.enter_state_playall_end()

        model = self.songs_table.model()
        # FIXME: think about a more elegant way
        reader = model.sourceModel()._reader
        if reader is not None:
            if reader.count is not None:
                task = task_spec.bind_blocking_io(reader.readall)
                self.toolbar.enter_state_playall_start()
                task.add_done_callback(reader_readall_cb)
                return
        songs = model.sourceModel().songs
        self._app.player.play_songs(songs=songs)

    async def show_model(self, model):
        model = self._app.library.cast_model_to_v1(model)
        model_type = ModelType(model.meta.model_type)
        if model_type == ModelType.album:
            renderer = AlbumRenderer(model)
        elif model_type == ModelType.artist:
            renderer = ArtistRenderer(model)
        elif model_type == ModelType.playlist:
            renderer = PlaylistRenderer(model)
        else:
            renderer = None
        await self.set_renderer(renderer)

    def show_collection(self, coll):
        renderer = SongsCollectionRenderer(coll)
        aio.create_task(self.set_renderer(renderer))

    def show_songs(self, songs=None, songs_g=None):
        """(DEPRECATED) provided only for backward compatibility"""
        warnings.warn('use readerer.show_songs please')
        renderer = Renderer()
        task = aio.create_task(self.set_renderer(renderer))
        if songs is not None:
            reader = wrap(songs)
        else:
            reader = songs_g
        task.add_done_callback(lambda _: renderer.show_songs(reader=reader))

    def show_albums_coll(self, albums_g):
        aio.create_task(self.set_renderer(AlbumsCollectionRenderer(albums_g)))

    def show_artists_coll(self, artists_g):
        aio.create_task(self.set_renderer(
            ArtistsCollectionRenderer(artists_g)))

    def show_player_playlist(self):
        aio.create_task(self.set_renderer(PlayerPlaylistRenderer()))

    def search(self, text):
        if self.isVisible() and self.songs_table is not None:
            self.songs_table.filter_row(text)

    def _add_songs_to_playlist(self, songs):
        for song in songs:
            self._app.playlist.add(song)

    def _songs_table_about_to_show_menu(self, ctx):
        add_action = ctx['add_action']
        models = ctx['models']
        if not models or models[0].meta.model_type != ModelType.song:
            return

        song = models[0]
        goto = self._app.browser.goto

        if self._app.library.check_flags_by_model(song, ProviderFlags.similar):
            add_action('相似歌曲', lambda *args: goto(model=song, path='/similar'))
        if self._app.library.check_flags_by_model(song,
                                                  ProviderFlags.hot_comments):
            add_action('歌曲评论',
                       lambda *args: goto(model=song, path='/hot_comments'))

    async def _on_songs_table_activated(self, index):
        """
        QTableView should have no IO operations.
        """
        from feeluown.widgets.songs import Column

        song = index.data(Qt.UserRole)
        if index.column() == Column.song:
            self.songs_table.play_song_needed.emit(song)
        else:
            try:
                song = await aio.run_in_executor(
                    None, self._app.library.song_upgrade, song)
            except NotSupported:
                assert ModelFlags.v2 & song.meta.flags
                self._app.show_msg('资源提供放不支持该功能')
                logger.info(
                    f'provider:{song.source} does not support song_get')
                song.state = ModelState.cant_upgrade
            except (ProviderIOError, RequestException) as e:
                # FIXME: we should only catch ProviderIOError here,
                # but currently, some plugins such fuo-qqmusic may raise
                # requests.RequestException
                logger.exception('upgrade song failed')
                self._app.show_msg(f'请求失败: {str(e)}')
            else:
                if index.column() == Column.artist:
                    artists = song.artists
                    if artists:
                        if len(artists) > 1:
                            self.songs_table.show_artists_by_index(index)
                        else:
                            self.songs_table.show_artist_needed.emit(
                                artists[0])
                elif index.column() == Column.album:
                    self.songs_table.show_album_needed.emit(song.album)
        model = self.songs_table.model()
        topleft = model.index(index.row(), 0)
        bottomright = model.index(index.row(), 4)
        model.dataChanged.emit(topleft, bottomright, [])
Example #8
0
class TableMetaWidget(QWidget):

    toggle_full_window_needed = pyqtSignal([bool])

    class getset_property:
        def __init__(self, name, set_cb):
            self.name = name
            self.name_real = '_' + name
            self.set_cb = set_cb

        def __get__(self, instance, owner):
            if hasattr(instance, self.name_real):
                return getattr(instance, self.name_real)
            return None

        def __set__(self, instance, value):
            setattr(instance, self.name_real, value)
            if self.set_cb is not None:
                self.set_cb(instance)

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.title_label = QLabel(self)
        self.cover_label = QLabel(self)
        self.meta_label = QLabel(self)
        self.desc_container = DescriptionContainer(parent=self)
        self.toolbar = SongsTableToolbar(parent=self)

        self.title_label.setTextFormat(Qt.RichText)
        self.meta_label.setTextFormat(Qt.RichText)

        self._is_fullwindow = False

        self._setup_ui()
        self._refresh()

        self.desc_container.space_pressed.connect(self.toggle_full_window)

    def _setup_ui(self):
        self.cover_label.setMinimumWidth(200)
        self.setMaximumHeight(180)
        self.title_label.setAlignment(Qt.AlignTop)
        self.meta_label.setAlignment(Qt.AlignTop)

        self._v_layout = QVBoxLayout(self)
        self._h_layout = QHBoxLayout()
        self._right_layout = QVBoxLayout()
        self._right_layout.addWidget(self.title_label)
        self._right_layout.addWidget(self.meta_label)
        self._right_layout.addWidget(self.desc_container)
        self._right_layout.addWidget(self.toolbar)
        self._right_layout.setAlignment(self.toolbar, Qt.AlignBottom)
        self._h_layout.addWidget(self.cover_label)
        self._h_layout.setAlignment(self.cover_label, Qt.AlignTop)
        self._h_layout.addLayout(self._right_layout)
        self._v_layout.addLayout(self._h_layout)

        self._h_layout.setContentsMargins(10, 10, 10, 10)
        self._h_layout.setSpacing(20)

        self._right_layout.setContentsMargins(0, 0, 0, 0)
        self._right_layout.setSpacing(5)

        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)

    def _refresh(self):
        self._refresh_title()
        self._refresh_meta_label()
        self._refresh_desc()
        self._refresh_cover()

    def _refresh_title(self):
        if self.title:
            self.title_label.show()
            self.title_label.setText('<h2>{}</h2>'.format(self.title))
        else:
            self.title_label.hide()

    def _refresh_meta_label(self):
        creator = self.creator
        creator_part = '👤 <a href="fuo://local/users/{}">{}</a>'\
            .format(creator, creator) if creator else ''
        if self.updated_at:
            updated_at = datetime.fromtimestamp(self.updated_at)
            updated_part = '🕒 更新于 <code style="font-size: small">{}</code>'\
                .format(updated_at.strftime('%Y-%m-%d'))
        else:
            updated_part = ''
        if self.created_at:
            created_at = datetime.fromtimestamp(self.created_at)
            created_part = '🕛 创建于 <code style="font-size: small">{}</code>'\
                .format(created_at.strftime('%Y-%m-%d'))
        else:
            created_part = ''
        if creator_part or updated_part or created_part:
            parts = [creator_part, created_part, updated_part]
            valid_parts = [p for p in parts if p]
            content = ' | '.join(valid_parts)
            text = '<span style="color: grey">{}</span>'.format(content)
            # TODO: add linkActivated callback for meta_label
            self.meta_label.setText(text)
            self.meta_label.show()
        else:
            self.meta_label.hide()

    def _refresh_desc(self):
        if self.desc:
            self.desc_container.show()
            self.desc_container.label.setText(self.desc)
        else:
            self.desc_container.hide()

    def _refresh_cover(self):
        if not self.cover:
            self.cover_label.hide()

    def _refresh_toolbar(self):
        if self.is_artist:
            self.toolbar.artist_mode()
        else:
            self.toolbar.songs_mode()

    def clear(self):
        self.title = None
        self.subtitle = None
        self.desc = None
        self.cover = None
        self.created_at = None
        self.updated_at = None
        self.creator = None
        self.is_artist = False

    def set_cover_pixmap(self, pixmap):
        self.cover_label.show()
        self.cover_label.setPixmap(
            pixmap.scaledToWidth(self.cover_label.width(),
                                 mode=Qt.SmoothTransformation))

    title = getset_property('title', _refresh_title)
    subtitle = getset_property('subtitle', _refresh_title)
    desc = getset_property('desc', _refresh_desc)
    cover = getset_property('cover', _refresh_cover)
    created_at = getset_property('created_at', _refresh_meta_label)
    updated_at = getset_property('updated_at', _refresh_meta_label)
    creator = getset_property('creator', _refresh_meta_label)
    is_artist = getset_property('is_artist', _refresh_toolbar)

    def toggle_full_window(self):
        if self._is_fullwindow:
            self.toggle_full_window_needed.emit(False)
            self.setMaximumHeight(180)
        else:
            # generally, display height will be less than 4000px
            self.toggle_full_window_needed.emit(True)
            self.setMaximumHeight(4000)
        self._is_fullwindow = not self._is_fullwindow