add artist-info-pane implementation - issue #268
Esse commit está contido em:
@@ -0,0 +1,793 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2014 fossfreedom
|
||||
# this module has been heavily modifed from rhythmbox context plugin
|
||||
# Copyright (C) 2009 John Iacona
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import re, os
|
||||
import cgi
|
||||
import urllib.request, urllib.parse
|
||||
import json
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
import rb
|
||||
import rb_lastfm as LastFM# from coverart-search-providers
|
||||
|
||||
from gi.repository import WebKit
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import RB
|
||||
from gi.repository import Gio
|
||||
from coverart_utils import create_pixbuf_from_file_at_size
|
||||
from coverart_utils import get_stock_size
|
||||
from coverart_widgets import PixbufButton
|
||||
from coverart_browser_prefs import GSetting
|
||||
|
||||
import gettext
|
||||
gettext.install('rhythmbox', RB.locale_dir())
|
||||
|
||||
def artist_exceptions(artist):
|
||||
exceptions = ['various']
|
||||
|
||||
for exception in exceptions:
|
||||
if exception in artist.lower():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def lastfm_datasource_link(path):
|
||||
return "<a href='http://last.fm/'><img src='%s/img/lastfm.png'></a>" % path
|
||||
|
||||
LASTFM_NO_ACCOUNT_ERROR = _("This information is only available to Last.fm users. Ensure the Last.fm plugin is enabled, select Last.fm in the side pane, and log in.")
|
||||
|
||||
def create_button_image(plugin, icon_name):
|
||||
path = 'img/'
|
||||
|
||||
return create_pixbuf_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, path + icon_name),
|
||||
*get_stock_size())
|
||||
|
||||
class ArtistInfoPane(GObject.GObject):
|
||||
__gsignals__ = {
|
||||
'selected' : (GObject.SIGNAL_RUN_LAST, None,
|
||||
(GObject.TYPE_STRING,GObject.TYPE_STRING))
|
||||
}
|
||||
|
||||
artist_info_paned_pos = GObject.property(type=str)
|
||||
|
||||
def __init__(self, button_box, scroll_window, info_paned, source):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.tab = {}
|
||||
self.ds = {}
|
||||
self.view = {}
|
||||
|
||||
self.buttons = button_box
|
||||
self.source = source
|
||||
self.plugin = source.plugin
|
||||
self.shell = source.shell
|
||||
self.info_scrolled_window = scroll_window
|
||||
self.info_paned = info_paned
|
||||
self.current_artist = None
|
||||
self.current_album_title = None
|
||||
|
||||
self.webview = WebKit.WebView()
|
||||
self.webview.connect("navigation-requested", self.navigation_request_cb)
|
||||
self.info_scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
self.info_scrolled_window.add (self.webview)
|
||||
self.info_scrolled_window.show_all()
|
||||
|
||||
# cache for artist/album information: valid for a month, can be used indefinitely
|
||||
# if offline, discarded if unused for six months
|
||||
self.info_cache = rb.URLCache(name = 'info',
|
||||
path = os.path.join('coverart_browser', 'info'),
|
||||
refresh = 30,
|
||||
discard = 180)
|
||||
# cache for rankings (artist top tracks and top albums): valid for a week,
|
||||
# can be used for a month if offline
|
||||
self.ranking_cache = rb.URLCache(name = 'ranking',
|
||||
path = os.path.join('coverart_browser', 'ranking'),
|
||||
refresh = 7,
|
||||
lifetime = 30)
|
||||
|
||||
self.info_cache.clean()
|
||||
self.ranking_cache.clean()
|
||||
|
||||
self.ds['link'] = LinksDataSource ()
|
||||
self.ds['artist'] = ArtistDataSource (self.info_cache, self.ranking_cache)
|
||||
self.view['artist'] = ArtistInfoView (self.shell, self.plugin, self.webview, self.ds['artist'], self.ds['link'])
|
||||
self.tab['artist'] = ArtistInfoTab (self.plugin, self.shell, self.buttons, self.ds['artist'], self.view['artist'])
|
||||
self.ds['album'] = AlbumDataSource(self.info_cache, self.ranking_cache)
|
||||
self.view['album'] = AlbumInfoView(self.shell, self.plugin, self.webview, self.ds['album'])
|
||||
self.tab['album'] = AlbumInfoTab(self.plugin, self.shell, self.buttons, self.ds['album'], self.view['album'])
|
||||
|
||||
self.gs = GSetting()
|
||||
self.connect_properties()
|
||||
self.connect_signals()
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50, self._change_paned_pos, self.source.viewmgr.view_name)
|
||||
self.current = 'artist'
|
||||
self.tab[self.current].activate ()
|
||||
|
||||
def connect_properties(self):
|
||||
'''
|
||||
Connects the source properties to the saved preferences.
|
||||
'''
|
||||
setting = self.gs.get_setting(self.gs.Path.PLUGIN)
|
||||
|
||||
setting.bind(
|
||||
self.gs.PluginKey.ARTIST_INFO_PANED_POSITION,
|
||||
self,
|
||||
'artist-info-paned-pos',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
def connect_signals(self):
|
||||
self.tab_cb_ids = []
|
||||
|
||||
# Listen for switch-tab signal from each tab
|
||||
for key, value in self.tab.items():
|
||||
self.tab_cb_ids.append((key, self.tab[key].connect ('switch-tab', self.change_tab)))
|
||||
|
||||
# Listen for selected signal from the views
|
||||
self.connect('selected', self.select_artist)
|
||||
|
||||
# lets remember info paned click
|
||||
self.info_paned.connect('button-release-event',
|
||||
self.artist_info_paned_button_release_callback)
|
||||
|
||||
# lets also listen for changes to the view to set the paned position
|
||||
self.source.viewmgr.connect('new-view', self.on_view_changed)
|
||||
|
||||
def on_view_changed(self, widget, view_name):
|
||||
self._change_paned_pos(view_name)
|
||||
|
||||
def _change_paned_pos(self, view_name):
|
||||
paned_positions = eval(self.artist_info_paned_pos)
|
||||
|
||||
found = None
|
||||
for viewpos in paned_positions:
|
||||
if view_name in viewpos:
|
||||
found = viewpos
|
||||
break
|
||||
|
||||
if not found:
|
||||
return
|
||||
|
||||
child_width = int(found.split(":")[1])
|
||||
|
||||
calc_pos = self.source.page.get_allocated_width() - child_width
|
||||
self.info_paned.set_position(calc_pos)
|
||||
self.info_paned.set_visible(True)
|
||||
|
||||
def artist_info_paned_button_release_callback(self, *args):
|
||||
'''
|
||||
Callback when the artist paned handle is released from its mouse click.
|
||||
'''
|
||||
child = self.info_paned.get_child2()
|
||||
child_width = child.get_allocated_width()
|
||||
|
||||
paned_positions = eval(self.artist_info_paned_pos)
|
||||
|
||||
found = None
|
||||
for viewpos in paned_positions:
|
||||
if self.source.viewmgr.view_name in viewpos:
|
||||
found = viewpos
|
||||
break
|
||||
|
||||
if not found:
|
||||
return
|
||||
|
||||
paned_positions.remove(found)
|
||||
paned_positions.append(self.source.viewmgr.view_name + ":" + str(child_width))
|
||||
|
||||
self.artist_info_paned_pos = repr(paned_positions)
|
||||
|
||||
def select_artist(self, widget, artist, album_title):
|
||||
self.tab[self.current].reload(artist, album_title)
|
||||
self.current_album_title = album_title
|
||||
self.current_artist = artist
|
||||
|
||||
def change_tab (self, tab, newtab):
|
||||
print("swapping tab from %s to %s" % (self.current, newtab))
|
||||
if (self.current != newtab):
|
||||
self.tab[self.current].deactivate()
|
||||
self.tab[newtab].activate(self.current_artist, self.current_album_title)
|
||||
self.current = newtab
|
||||
|
||||
def navigation_request_cb(self, view, frame, request):
|
||||
# open HTTP URIs externally. this isn't a web browser.
|
||||
if request.get_uri().startswith('http'):
|
||||
print("opening uri %s" % request.get_uri())
|
||||
Gtk.show_uri(self.shell.props.window.get_screen(), request.get_uri(), Gdk.CURRENT_TIME)
|
||||
|
||||
return 1 # WEBKIT_NAVIGATION_RESPONSE_IGNORE
|
||||
else:
|
||||
return 0 # WEBKIT_NAVIGATION_RESPONSE_ACCEPT
|
||||
|
||||
class ArtistInfoTab (GObject.GObject):
|
||||
|
||||
__gsignals__ = {
|
||||
'switch-tab' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
|
||||
(GObject.TYPE_STRING,))
|
||||
}
|
||||
|
||||
def __init__ (self, plugin, shell, buttons, ds, view):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.shell = shell
|
||||
self.sp = shell.props.shell_player
|
||||
self.db = shell.props.db
|
||||
self.buttons = buttons
|
||||
|
||||
self.button = PixbufButton()#Gtk.ToggleButton (label=_("Artist"))
|
||||
self.button.set_image(create_button_image(plugin, "microphone.png"))
|
||||
self.datasource = ds
|
||||
self.view = view
|
||||
self.album_title= None
|
||||
self.artist = None
|
||||
self.active = False
|
||||
|
||||
self.button.show()
|
||||
self.button.set_relief (Gtk.ReliefStyle.NONE)
|
||||
self.button.set_focus_on_click(False)
|
||||
self.button.connect ('clicked',
|
||||
lambda button : self.emit('switch-tab', 'artist'))
|
||||
buttons.pack_start (self.button, False, True, 0)
|
||||
|
||||
def activate (self, artist=None, album_title=None):
|
||||
print("activating Artist Tab")
|
||||
self.button.set_active(True)
|
||||
self.active = True
|
||||
self.reload (artist, album_title)
|
||||
|
||||
def deactivate (self):
|
||||
print("deactivating Artist Tab")
|
||||
self.button.set_active(False)
|
||||
self.active = False
|
||||
|
||||
def reload (self, artist, album_title):
|
||||
if not artist:
|
||||
return
|
||||
|
||||
if self.active and artist_exceptions(artist):
|
||||
print ("blank")
|
||||
self.view.blank_view()
|
||||
return
|
||||
|
||||
if self.active and ( (not self.artist or self.artist != artist)
|
||||
or (not self.album_title or self.album_title != album_title)
|
||||
):
|
||||
print ("now loading")
|
||||
self.view.loading (artist, album_title)
|
||||
print ("active")
|
||||
self.datasource.fetch_artist_data (artist)
|
||||
else:
|
||||
print ("load_view")
|
||||
self.view.load_view()
|
||||
|
||||
self.album_title = album_title
|
||||
self.artist = artist
|
||||
|
||||
class ArtistInfoView (GObject.GObject):
|
||||
|
||||
def __init__ (self, shell, plugin, webview, ds, link_ds):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.webview = webview
|
||||
self.ds = ds
|
||||
self.link_ds = link_ds
|
||||
self.shell = shell
|
||||
self.plugin = plugin
|
||||
self.file = ""
|
||||
|
||||
plugindir = plugin.plugin_info.get_data_dir()
|
||||
self.basepath = "file://" + urllib.request.pathname2url (plugindir)
|
||||
self.link_images = self.basepath + '/img/links/'
|
||||
|
||||
self.load_tmpl ()
|
||||
self.connect_signals ()
|
||||
|
||||
def load_view (self):
|
||||
self.webview.load_string (self.file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def blank_view (self):
|
||||
render_file = self.empty_template.render( stylesheet = self.styles )
|
||||
self.webview.load_string (render_file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def loading (self, current_artist, current_album_title):
|
||||
self.link_ds.set_artist (current_artist)
|
||||
self.link_ds.set_album (current_album_title)
|
||||
self.loading_file = self.loading_template.render (
|
||||
artist = current_artist,
|
||||
info = _("Loading biography for %s") % current_artist,
|
||||
song = "",
|
||||
basepath = self.basepath)
|
||||
self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def load_tmpl (self):
|
||||
path = rb.find_plugin_file(self.plugin, 'tmpl/artist-tmpl.html')
|
||||
empty_path = rb.find_plugin_file(self.plugin, 'tmpl/artist_empty-tmpl.html')
|
||||
loading_path = rb.find_plugin_file (self.plugin, 'tmpl/loading.html')
|
||||
self.template = Template (filename = path)
|
||||
self.loading_template = Template (filename = loading_path)
|
||||
self.empty_template = Template (filename = empty_path)
|
||||
self.styles = self.basepath + '/tmpl/artistmain.css'
|
||||
|
||||
def connect_signals (self):
|
||||
self.air_id = self.ds.connect ('artist-info-ready', self.artist_info_ready)
|
||||
|
||||
def artist_info_ready (self, ds):
|
||||
# Can only be called after the artist-info-ready signal has fired.
|
||||
# If called any other time, the behavior is undefined
|
||||
try:
|
||||
info = ds.get_artist_info ()
|
||||
|
||||
small, med, big = info['images'] or (None, None, None)
|
||||
summary, full_bio = info['bio'] or (None, None)
|
||||
|
||||
link_album = self.link_ds.get_album()
|
||||
if not link_album:
|
||||
link_album = ""
|
||||
|
||||
links = self.link_ds.get_album_links()
|
||||
if not links:
|
||||
links = {}
|
||||
|
||||
self.file = self.template.render (artist = ds.get_current_artist (),
|
||||
error = ds.get_error (),
|
||||
image = med,
|
||||
fullbio = full_bio,
|
||||
shortbio = summary,
|
||||
datasource = lastfm_datasource_link (self.basepath),
|
||||
stylesheet = self.styles,
|
||||
album = link_album,
|
||||
art_links = self.link_ds.get_artist_links (),
|
||||
alb_links = links,
|
||||
link_images= self.link_images,
|
||||
similar = ds.get_similar_info() )
|
||||
self.load_view ()
|
||||
except Exception as e:
|
||||
print("Problem in info ready: %s" % e)
|
||||
|
||||
|
||||
class ArtistDataSource (GObject.GObject):
|
||||
|
||||
__gsignals__ = {
|
||||
'artist-info-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
|
||||
}
|
||||
|
||||
def __init__ (self, info_cache, ranking_cache):
|
||||
GObject.GObject.__init__ (self)
|
||||
|
||||
self.current_artist = None
|
||||
self.error = None
|
||||
#' 'signal' : 'artist-info-ready', '
|
||||
self.artist = {
|
||||
'info' : {
|
||||
'data' : None,
|
||||
'function' : 'getinfo',
|
||||
'cache' : info_cache,
|
||||
'signal' : 'artist-info-ready',
|
||||
'parsed' : False
|
||||
},
|
||||
'similar' : {
|
||||
'data' : None,
|
||||
'function' : 'getsimilar',
|
||||
'cache' : info_cache,
|
||||
'signal' : 'artist-info-ready',
|
||||
'parsed' : False
|
||||
}
|
||||
}
|
||||
|
||||
def fetch_artist_data (self, artist):
|
||||
"""
|
||||
Initiate the fetching of all artist data. Fetches artist info, similar
|
||||
artists, artist top albums and top tracks. Downloads XML files from last.fm
|
||||
and saves as parsed DOM documents in self.artist dictionary. Must be called
|
||||
before any of the get_* methods.
|
||||
"""
|
||||
self.current_artist = artist
|
||||
if LastFM.user_has_account() is False:
|
||||
self.error = LASTFM_NO_ACCOUNT_ERROR
|
||||
self.emit ('artist-info-ready')
|
||||
return
|
||||
|
||||
self.error = None
|
||||
artist = urllib.parse.quote_plus(artist)
|
||||
self.fetched = 0
|
||||
for key, value in self.artist.items():
|
||||
print ("search")
|
||||
cachekey = "lastfm:artist:%sjson:%s" % (value['function'], artist)
|
||||
url = '%s?method=artist.%s&artist=%s&limit=10&api_key=%s&format=json' % (LastFM.API_URL,
|
||||
value['function'], artist, LastFM.API_KEY)
|
||||
print("fetching %s" % url)
|
||||
value['cache'].fetch(cachekey, url, self.fetch_artist_data_cb, value)
|
||||
|
||||
def fetch_artist_data_cb (self, data, category):
|
||||
if data is None:
|
||||
print("no data fetched for artist %s" % category['function'])
|
||||
return
|
||||
|
||||
print (category)
|
||||
try:
|
||||
category['data'] = json.loads(data.decode('utf-8'))
|
||||
category['parsed'] = False
|
||||
self.fetched += 1
|
||||
if self.fetched == len(self.artist):
|
||||
self.emit (category['signal'])
|
||||
|
||||
except Exception as e:
|
||||
print("Error parsing artist %s: %s" % (category['function'], e))
|
||||
return False
|
||||
|
||||
def get_current_artist (self):
|
||||
return self.current_artist
|
||||
|
||||
def get_error (self):
|
||||
return self.error
|
||||
|
||||
def get_artist_images (self):
|
||||
"""
|
||||
Returns tuple of image url's for small, medium, and large images.
|
||||
"""
|
||||
data = self.artist['info']['data']
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
images = [img['#text'] for img in data['artist'].get('image', ())]
|
||||
return images[:3]
|
||||
|
||||
def get_artist_bio (self):
|
||||
"""
|
||||
Returns tuple of summary and full bio
|
||||
"""
|
||||
data = self.artist['info']['data']
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if not self.artist['info']['parsed']:
|
||||
content = data['artist']['bio']['content']
|
||||
summary = data['artist']['bio']['summary']
|
||||
return summary, content
|
||||
|
||||
return self.artist['info']['data']['bio']
|
||||
|
||||
def get_similar_info(self):
|
||||
"""
|
||||
Returns the dictionary { 'images', 'bio' }
|
||||
"""
|
||||
if not self.artist['similar']['parsed']:
|
||||
json_artists_data = self.artist['similar']['data']['similarartists']
|
||||
|
||||
results = []
|
||||
for json_artist in json_artists_data["artist"]:
|
||||
name = json_artist["name"]
|
||||
image_url = json_artist["image"][1]["#text"]
|
||||
similarity = int(100 * float(json_artist["match"]))
|
||||
|
||||
results.append( {'name': name,
|
||||
'image_url': image_url,
|
||||
'similarity': similarity})
|
||||
|
||||
self.artist['similar']['data'] = results
|
||||
self.artist['similar']['parsed'] = True
|
||||
|
||||
return self.artist['similar']['data']
|
||||
|
||||
def get_artist_info (self):
|
||||
"""
|
||||
Returns the dictionary { 'images', 'bio' }
|
||||
"""
|
||||
if not self.artist['info']['parsed']:
|
||||
images = self.get_artist_images()
|
||||
bio = self.get_artist_bio()
|
||||
self.artist['info']['data'] = { 'images' : images,
|
||||
'bio' : bio }
|
||||
self.artist['info']['parsed'] = True
|
||||
|
||||
return self.artist['info']['data']
|
||||
|
||||
class LinksDataSource (GObject.GObject):
|
||||
|
||||
def __init__ (self):
|
||||
GObject.GObject.__init__ (self)
|
||||
print ("init")
|
||||
self.entry = None
|
||||
self.error = None
|
||||
|
||||
self.artist = None
|
||||
self.album = None
|
||||
|
||||
def set_artist (self, artist):
|
||||
print ("set_artist")
|
||||
self.artist = artist
|
||||
|
||||
def get_artist (self):
|
||||
print ("get_artist")
|
||||
return self.artist
|
||||
|
||||
def set_album (self, album):
|
||||
self.album = album
|
||||
|
||||
def get_album (self):
|
||||
return self.album
|
||||
|
||||
def get_artist_links (self):
|
||||
"""
|
||||
Return a dictionary with artist URLs to popular music databases and
|
||||
encyclopedias.
|
||||
"""
|
||||
print ("get_artist_links")
|
||||
artist = self.get_artist()
|
||||
if artist is not "" and artist is not None:
|
||||
wpartist = artist.replace(" ", "_")
|
||||
artist = urllib.parse.quote_plus(artist)
|
||||
artist_links = {
|
||||
"Wikipedia" : "http://www.wikipedia.org/wiki/%s" % wpartist,
|
||||
"Discogs" : "http://www.discogs.com/artist/%s" % artist,
|
||||
"Allmusic" : "http://www.allmusic.com/search/artist/%s" % artist
|
||||
}
|
||||
return artist_links
|
||||
return None
|
||||
|
||||
def get_album_links (self):
|
||||
"""
|
||||
Return a dictionary with album URLs to popular music databases and
|
||||
encyclopedias.
|
||||
"""
|
||||
print ("get_album_links")
|
||||
album = self.get_album()
|
||||
print (album)
|
||||
if album is not None and album is not "":
|
||||
print ("obtaining links")
|
||||
wpalbum = album.replace(" ", "_")
|
||||
album = urllib.parse.quote_plus(album)
|
||||
album_links = {
|
||||
"Wikipedia" : "http://www.wikipedia.org/wiki/%s" % wpalbum,
|
||||
"Discogs" : "http://www.discogs.com/search?type=album&q=%s&f=html" % album,
|
||||
"Allmusic" : "http://allmusic.com/search/album/%s" % album
|
||||
}
|
||||
return album_links
|
||||
return None
|
||||
|
||||
def get_error (self):
|
||||
if self.get_artist() is "":
|
||||
return _("No artist specified.")
|
||||
|
||||
class AlbumInfoTab (GObject.GObject):
|
||||
|
||||
__gsignals__ = {
|
||||
'switch-tab' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
|
||||
(GObject.TYPE_STRING,))
|
||||
}
|
||||
|
||||
def __init__ (self, plugin, shell, buttons, ds, view):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.shell = shell
|
||||
self.sp = shell.props.shell_player
|
||||
self.db = shell.props.db
|
||||
self.buttons = buttons
|
||||
|
||||
#self.button = Gtk.ToggleButton (label=_("Albums"))
|
||||
self.button = PixbufButton()
|
||||
self.button.set_image(create_button_image(plugin, "covermgr.png"))
|
||||
self.ds = ds
|
||||
self.view = view
|
||||
self.album_title= None
|
||||
self.artist = None
|
||||
self.active = False
|
||||
|
||||
self.button.show()
|
||||
self.button.set_relief (Gtk.ReliefStyle.NONE)
|
||||
self.button.set_focus_on_click(False)
|
||||
self.button.connect ('clicked',
|
||||
lambda button: self.emit ('switch-tab', 'album'))
|
||||
buttons.pack_start (self.button, False, True, 0)
|
||||
|
||||
def activate (self, artist, album_title):
|
||||
self.button.set_active(True)
|
||||
self.active = True
|
||||
self.reload (artist, album_title)
|
||||
|
||||
def deactivate (self):
|
||||
self.button.set_active(False)
|
||||
self.active = False
|
||||
|
||||
def reload (self, artist, album_title):
|
||||
if not artist:
|
||||
return
|
||||
|
||||
if self.active and artist_exceptions(artist):
|
||||
print ("blank")
|
||||
self.view.blank_view()
|
||||
return
|
||||
|
||||
if self.active and (not self.artist or artist != self.artist):
|
||||
self.view.loading(artist, album_title)
|
||||
self.ds.fetch_album_list (artist)
|
||||
else:
|
||||
self.view.load_view()
|
||||
|
||||
self.album_title = album_title
|
||||
self.artist = artist
|
||||
|
||||
class AlbumInfoView (GObject.GObject):
|
||||
|
||||
def __init__ (self, shell, plugin, webview, ds):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.webview = webview
|
||||
self.ds = ds
|
||||
self.shell = shell
|
||||
self.plugin = plugin
|
||||
self.file = ""
|
||||
|
||||
plugindir = plugin.plugin_info.get_data_dir()
|
||||
self.basepath = "file://" + urllib.request.pathname2url (plugindir)
|
||||
|
||||
self.load_tmpl ()
|
||||
self.connect_signals ()
|
||||
|
||||
def load_view (self):
|
||||
self.webview.load_string(self.file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def blank_view (self):
|
||||
render_file = self.empty_template.render( stylesheet = self.styles )
|
||||
self.webview.load_string (render_file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def connect_signals (self):
|
||||
self.ds.connect('albums-ready', self.album_list_ready)
|
||||
|
||||
def loading (self, current_artist, current_album_title):
|
||||
self.loading_file = self.loading_template.render (
|
||||
artist = current_artist,
|
||||
# Translators: 'top' here means 'most popular'. %s is replaced by the artist name.
|
||||
info = _("Loading top albums for %s") % current_artist,
|
||||
song = "",
|
||||
basepath = self.basepath)
|
||||
self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
|
||||
|
||||
def load_tmpl (self):
|
||||
path = rb.find_plugin_file (self.plugin, 'tmpl/album-tmpl.html')
|
||||
empty_path = rb.find_plugin_file (self.plugin, 'tmpl/album_empty-tmpl.html')
|
||||
self.loading_path = rb.find_plugin_file (self.plugin, 'tmpl/loading.html')
|
||||
self.album_template = Template (filename = path)
|
||||
self.loading_template = Template (filename = self.loading_path)
|
||||
self.empty_template = Template (filename = empty_path)
|
||||
self.styles = self.basepath + '/tmpl/artistmain.css'
|
||||
|
||||
def album_list_ready (self, ds):
|
||||
self.file = self.album_template.render (error = ds.get_error(),
|
||||
albums = ds.get_top_albums(),
|
||||
artist = ds.get_artist(),
|
||||
datasource = lastfm_datasource_link (self.basepath),
|
||||
stylesheet = self.styles)
|
||||
self.load_view ()
|
||||
|
||||
|
||||
class AlbumDataSource (GObject.GObject):
|
||||
|
||||
__gsignals__ = {
|
||||
'albums-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
|
||||
}
|
||||
|
||||
def __init__ (self, info_cache, ranking_cache):
|
||||
GObject.GObject.__init__ (self)
|
||||
self.albums = None
|
||||
self.error = None
|
||||
self.artist = None
|
||||
self.max_albums_fetched = 8
|
||||
self.fetching = 0
|
||||
self.info_cache = info_cache
|
||||
self.ranking_cache = ranking_cache
|
||||
|
||||
def get_artist (self):
|
||||
return self.artist
|
||||
|
||||
def get_error (self):
|
||||
return self.error
|
||||
|
||||
def fetch_album_list (self, artist):
|
||||
if LastFM.user_has_account() is False:
|
||||
self.error = LASTFM_NO_ACCOUNT_ERROR
|
||||
self.emit ('albums-ready')
|
||||
return
|
||||
|
||||
self.artist = artist
|
||||
qartist = urllib.parse.quote_plus(artist)
|
||||
self.error = None
|
||||
url = "%s?method=artist.gettopalbums&artist=%s&api_key=%s&format=json" % (
|
||||
LastFM.API_URL, qartist, LastFM.API_KEY)
|
||||
print (url)
|
||||
cachekey = 'lastfm:artist:gettopalbumsjson:%s' % qartist
|
||||
self.ranking_cache.fetch(cachekey, url, self.parse_album_list, artist)
|
||||
|
||||
def parse_album_list (self, data, artist):
|
||||
if data is None:
|
||||
print("Nothing fetched for %s top albums" % artist)
|
||||
return False
|
||||
|
||||
try:
|
||||
parsed = json.loads(data.decode("utf-8"))
|
||||
except Exception as e:
|
||||
print("Error parsing album list: %s" % e)
|
||||
return False
|
||||
|
||||
self.error = parsed.get('error')
|
||||
if self.error:
|
||||
self.emit ('albums-ready')
|
||||
return False
|
||||
|
||||
albums = parsed['topalbums'].get('album', [])
|
||||
if len(albums) == 0:
|
||||
self.error = "No albums found for %s" % artist
|
||||
self.emit('albums-ready')
|
||||
return True
|
||||
|
||||
self.albums = []
|
||||
albums = parsed['topalbums'].get('album', [])[:self.max_albums_fetched]
|
||||
self.fetching = len(albums)
|
||||
for i, a in enumerate(albums):
|
||||
images = [img['#text'] for img in a.get('image', [])]
|
||||
self.albums.append({'title': a.get('name'), 'images': images[:3]})
|
||||
self.fetch_album_info(artist, a.get('name'), i)
|
||||
|
||||
return True
|
||||
|
||||
def get_top_albums (self):
|
||||
return self.albums
|
||||
|
||||
def fetch_album_info (self, artist, album, index):
|
||||
qartist = urllib.parse.quote_plus(artist)
|
||||
qalbum = urllib.parse.quote_plus(album)
|
||||
cachekey = "lastfm:album:getinfojson:%s:%s" % (qartist, qalbum)
|
||||
url = "%s?method=album.getinfo&artist=%s&album=%s&api_key=%s&format=json" % (
|
||||
LastFM.API_URL, qartist, qalbum, LastFM.API_KEY)
|
||||
self.info_cache.fetch(cachekey, url, self.parse_album_info, album, index)
|
||||
|
||||
def parse_album_info (self, data, album, index):
|
||||
rv = True
|
||||
try:
|
||||
parsed = json.loads(data.decode('utf-8'))
|
||||
self.albums[index]['id'] = parsed['album']['id']
|
||||
|
||||
for k in ('releasedate', 'summary'):
|
||||
self.albums[index][k] = parsed['album'].get(k)
|
||||
|
||||
tracklist = []
|
||||
tracks = parsed['album']['tracks'].get('track', [])
|
||||
for i, t in enumerate(tracks):
|
||||
title = t['name']
|
||||
duration = int(t['duration'])
|
||||
tracklist.append((i, title, duration))
|
||||
|
||||
self.albums[index]['tracklist'] = tracklist
|
||||
self.albums[index]['duration'] = sum([t[2] for t in tracklist])
|
||||
|
||||
if 'wiki' in parsed['album']:
|
||||
self.albums[index]['wiki-summary'] = parsed['album']['wiki']['summary']
|
||||
self.albums[index]['wiki-content'] = parsed['album']['wiki']['content']
|
||||
|
||||
except Exception as e:
|
||||
print("Error parsing album tracklist: %s" % e)
|
||||
rv = False
|
||||
|
||||
self.fetching -= 1
|
||||
print("%s albums left to process" % self.fetching)
|
||||
if self.fetching == 0:
|
||||
self.emit('albums-ready')
|
||||
|
||||
return rv
|
||||
|
||||
@@ -958,6 +958,7 @@ class ArtistView(Gtk.TreeView, AbstractView):
|
||||
active_object = self.artist_manager.model.get_from_path(treepath)
|
||||
|
||||
if not isinstance(active_object, Album):
|
||||
self.source.artist_info.emit('selected', active_object.name, None)
|
||||
if self.icon_automatic:
|
||||
# reset counter so that we get correct double click action for albums
|
||||
self.source.click_count = 0
|
||||
|
||||
@@ -164,7 +164,8 @@ class GSetting:
|
||||
WEBKIT='webkit-support',
|
||||
ARTIST_PANED_POSITION='artist-paned-pos',
|
||||
ARTIST_PANED_DISPLAY='artist-paned-display',
|
||||
USE_FAVOURITES='use-favourites')
|
||||
USE_FAVOURITES='use-favourites',
|
||||
ARTIST_INFO_PANED_POSITION='artist-info-paned-pos')
|
||||
|
||||
self.setting = {}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from gi.repository import Gio
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import RB
|
||||
from gi.repository import WebKit
|
||||
|
||||
from coverart_album import AlbumManager
|
||||
from coverart_entryview import CoverArtEntryView as EV
|
||||
@@ -44,6 +45,7 @@ from coverart_coverflowview import CoverFlowView
|
||||
from coverart_artistview import ArtistView
|
||||
from coverart_listview import ListView
|
||||
from coverart_toolbar import ToolbarManager
|
||||
from coverart_artistinfo import ArtistInfoPane
|
||||
|
||||
import coverart_rb3compat as rb3compat
|
||||
import random
|
||||
@@ -230,6 +232,16 @@ class CoverArtBrowserSource(RB.Source):
|
||||
self.request_cancel_button = ui.get_object('request_cancel_button')
|
||||
self.paned = ui.get_object('paned')
|
||||
self.notebook = ui.get_object('bottom_notebook')
|
||||
|
||||
#---- set up info pane -----#
|
||||
info_scrolled_window = ui.get_object('info_scrolled_window')
|
||||
info_button_box = ui.get_object('info_button_box')
|
||||
artist_info_paned = ui.get_object('vertical_info_paned')
|
||||
|
||||
self.artist_info = ArtistInfoPane(info_button_box,
|
||||
info_scrolled_window,
|
||||
artist_info_paned,
|
||||
self)
|
||||
|
||||
# quick search
|
||||
self.quick_search = ui.get_object('quick_search_entry')
|
||||
@@ -338,7 +350,7 @@ class CoverArtBrowserSource(RB.Source):
|
||||
Callback when the artist paned handle is released from its mouse click.
|
||||
'''
|
||||
self.artist_paned_pos = self.artist_paned.get_position()
|
||||
|
||||
|
||||
def display_quick_artist_filter_callback(self):
|
||||
if self.artist_treeview.get_visible():
|
||||
self.artist_treeview.set_visible(False)
|
||||
@@ -786,6 +798,9 @@ class CoverArtBrowserSource(RB.Source):
|
||||
else:
|
||||
self.stars.set_rating(0)
|
||||
|
||||
if len(selected) == 1:
|
||||
self.artist_info.emit('selected', selected[0].artist, selected[0].name)
|
||||
|
||||
for album in selected:
|
||||
# add the album to the entry_view
|
||||
self.entry_view.add_album(album)
|
||||
@@ -1045,7 +1060,7 @@ class Views:
|
||||
class ViewManager(GObject.Object):
|
||||
# signals
|
||||
__gsignals__ = {
|
||||
'new-view': (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
'new-view': (GObject.SIGNAL_RUN_LAST, None, (str,))
|
||||
}
|
||||
|
||||
# properties
|
||||
@@ -1136,7 +1151,7 @@ class ViewManager(GObject.Object):
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
setting[gs.PluginKey.VIEW_NAME] = saved_view
|
||||
|
||||
self.emit('new-view')
|
||||
self.emit('new-view', self.view_name)
|
||||
|
||||
def get_view_icon_name(self, view_name):
|
||||
return self._views[view_name].get_view_icon_name()
|
||||
|
||||
@@ -248,7 +248,7 @@ class PixbufButton(EnhancedButton):
|
||||
image = Gtk.Image()
|
||||
super(PixbufButton, self).set_image(image)
|
||||
|
||||
if not self.controller.enabled:
|
||||
if hasattr(self, "controller.enabled") and not self.controller.enabled:
|
||||
pixbuf = self._getBlendedPixbuf(pixbuf)
|
||||
|
||||
self.get_image().set_from_pixbuf(pixbuf)
|
||||
|
||||
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 1.9 KiB |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 803 B |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 771 B |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 381 B |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 4.9 KiB |
@@ -206,6 +206,11 @@
|
||||
<summary>Display the artist filter pane</summary>
|
||||
<description>Display the artist filter pane</description>
|
||||
</key>
|
||||
<key type="s" name="artist-info-paned-pos">
|
||||
<default>"['coverview:0','artistview:250','coverflowview:0']"</default>
|
||||
<summary>Position of the artist info paned.</summary>
|
||||
<description>Position of the artist info paned.</description>
|
||||
</key>
|
||||
<key type="b" name="use-favourites">
|
||||
<default>false</default>
|
||||
<summary>Use favourites</summary>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<%page args="error, albums, artist, stylesheet, datasource" />
|
||||
<html> <head> <meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<%!
|
||||
import re
|
||||
import cgi
|
||||
import email.utils
|
||||
from gettext import ngettext
|
||||
|
||||
def cleanup(text):
|
||||
return re.sub(r'\([^\)]*\)', '', text)
|
||||
|
||||
def sec2hms(time):
|
||||
hr = int(time / 3600)
|
||||
if hr > 0:
|
||||
time %= 3600
|
||||
mn = time / 60
|
||||
sec = time % 60
|
||||
if hr > 0:
|
||||
return _("%d:%02d:%02d") % (hr,mn,sec)
|
||||
else:
|
||||
return _("%d:%02d") %(mn,sec)
|
||||
|
||||
def format_year(date):
|
||||
try:
|
||||
parsed = email.utils.parsedate(date)
|
||||
except Exception as e:
|
||||
return ""
|
||||
if parsed is None:
|
||||
return ""
|
||||
else:
|
||||
return '[' + str(parsed[0]) + ']'
|
||||
|
||||
%>
|
||||
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
|
||||
<script language="javascript">
|
||||
function swapClass (element, klass1, klass2) {
|
||||
elt = document.getElementById(element);
|
||||
elt.className = (elt.className == klass1) ? klass2 : klass1;
|
||||
}
|
||||
function swapText (element, text1, text2) {
|
||||
elt = document.getElementById(element);
|
||||
elt.innerHTML = (elt.innerHTML == text1) ? text2 : text1;
|
||||
}
|
||||
function toggle_vis (element) {
|
||||
swapClass(element, 'hidden', 'shown');
|
||||
hide = ${ '"' + _("Hide all tracks") + '"' };
|
||||
show = ${ '"' + _("Show all tracks") + '"' };
|
||||
swapText('btn_'+element, hide, show);
|
||||
}
|
||||
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.wiki
|
||||
{ font-size: 10pt;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
%if error is None:
|
||||
<%
|
||||
num_albums = len(albums)
|
||||
%>
|
||||
<h1>${ _("Top albums by %s") % ("<em>" + cgi.escape(artist, True) + "</em>") }</h1>
|
||||
%for i, entry in enumerate(albums) :
|
||||
<%
|
||||
if 'tracklist' not in entry or len(entry['tracklist']) == 0:
|
||||
continue
|
||||
%>
|
||||
<div id="album${entry['id'] | h}" class="album">
|
||||
<img width="64" src="${entry['images'][1] | h}" alt="${entry['images'] | h}"/>
|
||||
<h2>${entry['title'] | h}
|
||||
%if 'releasedate' in entry:
|
||||
${ format_year(entry['releasedate']) }
|
||||
%endif
|
||||
</h2>
|
||||
%if 'duration' in entry:
|
||||
<%
|
||||
album_time = sec2hms(entry['duration'])
|
||||
tracks = len(entry['tracklist'])
|
||||
s = ngettext("%s (%d track)", "%s (%d tracks)", tracks)
|
||||
%>
|
||||
<p class="duration">${ s % (album_time, tracks) }</p>
|
||||
%endif
|
||||
%if 'tracklist' in entry:
|
||||
<button id="btn_${entry['id'] | h}" onclick="toggle_vis(${entry['id'] | h})">
|
||||
${ _("Show all tracks") }
|
||||
</button>
|
||||
<table class="hidden" id="${entry['id'] | h}">
|
||||
%for num, title, time in entry['tracklist'] :
|
||||
<%
|
||||
time = sec2hms(time)
|
||||
title = cleanup(title)
|
||||
num = num+1
|
||||
%>
|
||||
<tr><td>${num}</td><td>${title | h}</td><td>${time}</td></tr>
|
||||
%endfor
|
||||
</table>
|
||||
%else:
|
||||
<p>${ _("Track list not available") }</p>
|
||||
%endif
|
||||
|
||||
<div class="wiki">
|
||||
%if 'wiki-summary' in entry:
|
||||
${entry['wiki-summary']}
|
||||
%endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
%endfor
|
||||
<p>${datasource}</p>
|
||||
%else:
|
||||
<h1>${ _("Unable to retrieve album information:") }</h1>
|
||||
<p class="error">${error | h}</p>
|
||||
%endif
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<%page args="stylesheet"/>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<%page args="artist, image, shortbio, fullbio, stylesheet, datasource, album, art_links, alb_links, link_images, similar" />
|
||||
<%!
|
||||
import re
|
||||
import cgi
|
||||
remove_links = re.compile ('</?a[^>]*> ',re.VERBOSE)
|
||||
|
||||
def cleanup(text):
|
||||
if text is None:
|
||||
return _("No information available")
|
||||
text = remove_links.sub ('', text)
|
||||
text = text.replace('\n', '</p><p>')
|
||||
return text
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
|
||||
<style type="text/css">
|
||||
|
||||
.shown p,
|
||||
.similar td,
|
||||
.links a
|
||||
{ font-size: 10pt;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.links img
|
||||
{ padding: 0 5px 0 5px }
|
||||
|
||||
.links ul
|
||||
{ list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="artist">
|
||||
%if error is None:
|
||||
<h1>${artist | h}</h1>
|
||||
<img src="${image | h}" />
|
||||
<div id="shortbio" class="shown">
|
||||
<% shortbio = cleanup(shortbio) %>
|
||||
<p>${shortbio}</p>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<h1>${ _("Links for %s:") % ("<em>" + artist + "</em>")}</h1>
|
||||
<ul>
|
||||
%for k, v in art_links.items() :
|
||||
<li><img src="${link_images}${k}16x16.png" /><a href="${v}">${k}</a></li>
|
||||
%endfor
|
||||
</ul>
|
||||
%if len(alb_links) > 0:
|
||||
<h1>${ _("Links for %s:") % ("<em>" + album + "</em>")}</h1>
|
||||
%endif
|
||||
<ul>
|
||||
%for k, v in alb_links.items() :
|
||||
<li><img src="${link_images}${k}16x16.png" /><a href="${v}">${k}</a></li>
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="similar">
|
||||
<h1>Similar Artists</h1>
|
||||
|
||||
<table id="similar">
|
||||
|
||||
%for i, entry in enumerate(similar) :
|
||||
<tr><td><img width="64" src="${entry['image_url'] | h}" alt="${entry['image_url'] | h}"/></td><td>${entry['name']} ${entry['similarity']}% similar</td></tr>
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>${datasource}</p>
|
||||
%else:
|
||||
<h1>${ _("Unable to retrieve artist information:") }</h1>
|
||||
<p class="error">${error | h}</p>
|
||||
%endif
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<%page args="stylesheet"/>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
body { padding: 6px; line-height: 1.4em }
|
||||
h1 { font-size: 100% }
|
||||
h2 { font-size: 90%; color: #555; line-height: 1em }
|
||||
h3 {padding: 0}
|
||||
ol { padding: 5px 0 0 15px }
|
||||
.hidden { display: none }
|
||||
.shown { display: block }
|
||||
.album img { float: left; margin: 0 5px 5px 0; padding: 1px; border: 1px solid #AAA }
|
||||
.artist img { float: left; margin: 0 7px 4px 0; padding: 1px; border: 1px solid #AAA }
|
||||
.album {clear: both; }
|
||||
.duration {font-size: 80%; font-style: italic}
|
||||
button {align: right}
|
||||
table {font-size: 100%; width: 100%; display:block; clear: both; }
|
||||
@@ -0,0 +1,30 @@
|
||||
<%page args="error, artist, album, art_links, alb_links, images, stylesheet" />
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
|
||||
<style type="text/css">
|
||||
img { padding: 0 5px 0 5px }
|
||||
ul { list-style-type: none }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
%if error is None:
|
||||
<h1>${ _("Links for %s:") % ("<em>" + artist + "</em>")}</h1>
|
||||
<ul>
|
||||
%for k, v in art_links.items() :
|
||||
<li><img src="${images}${k}16x16.png" /><a href="${v}">${k}</a></li>
|
||||
%endfor
|
||||
</ul>
|
||||
<h1>${ _("Links for %s:") % ("<em>" + album + "</em>")}</h1>
|
||||
<ul>
|
||||
%for k, v in alb_links.items() :
|
||||
<li><img src="${images}${k}16x16.png" /><a href="${v}">${k}</a></li>
|
||||
%endfor
|
||||
</ul>
|
||||
%else:
|
||||
<h1>${ _("Unable to get links") }</h1>
|
||||
<p class="error">${error | h}</p>
|
||||
%endif
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text-html; charset=utf-8">
|
||||
<style type="text/css">
|
||||
body { padding: 6px; line-height: 1.4em }
|
||||
h1 { font-size: 130% }
|
||||
img { display: block; margin-left: auto; margin-right: auto }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${info | h}</h1>
|
||||
<img src="${basepath}/img/spinner.gif" />
|
||||
</body>
|
||||
</html>
|
||||
+57
-15
@@ -10,7 +10,7 @@
|
||||
<object class="GtkListStore" id="liststore1">
|
||||
<columns>
|
||||
<!-- column-name artist name -->
|
||||
<column type="gchararray"/>
|
||||
<column type="gchararray" />
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_box">
|
||||
@@ -45,14 +45,14 @@
|
||||
<property name="model">liststore1</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="artist_treeview_selection">
|
||||
<signal name="changed" handler="on_artist_treeview_selection_changed" swapped="no"/>
|
||||
<signal name="changed" handler="on_artist_treeview_selection_changed" swapped="no" />
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="treeviewcolumn1">
|
||||
<property name="title" translatable="yes">Track Artist</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext1" />
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
@@ -61,13 +61,19 @@
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="vertical_info_paned">
|
||||
<property name="visible">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="position">0</property>
|
||||
<child>
|
||||
<object class="GtkOverlay" id="iconview_overlay">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
@@ -77,15 +83,14 @@
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
</child>
|
||||
<child></child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="QuickSearchEntry" id="quick_search_entry">
|
||||
<property name="shadow_type">out</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="valign">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -94,26 +99,63 @@
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
|
||||
<child>
|
||||
<object class="GtkBox" id="box1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="info_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="info_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child></child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
|
||||
<child>
|
||||
<object class="GtkNotebook" id="bottom_notebook">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="tab_pos">bottom</property>
|
||||
<signal name="switch-page" handler="notebook_switch_page_callback" swapped="no"/>
|
||||
<signal name="switch-page" handler="notebook_switch_page_callback" swapped="no" />
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_label">
|
||||
@@ -173,7 +215,7 @@
|
||||
<property name="tooltip_text" translatable="yes">Stop coverart fetch.</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="image">cancel-icon</property>
|
||||
<signal name="clicked" handler="cancel_request_callback" swapped="no"/>
|
||||
<signal name="clicked" handler="cancel_request_callback" swapped="no" />
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário