Draw channel stats (unplayed, downloaded) on channel navigator
git-svn-id: svn://svn.berlios.de/gpodder/trunk@474 b0d088ad-0a06-0410-aad2-9ed5178a7e87
This commit is contained in:
parent
e8b8e1bd0c
commit
1bff3e1227
5 changed files with 226 additions and 52 deletions
14
ChangeLog
14
ChangeLog
|
@ -1,3 +1,17 @@
|
|||
Tue, 27 Nov 2007 22:59:26 +0100 <thp@perli.net>
|
||||
Draw channel stats (unplayed, downloaded) on channel navigator
|
||||
|
||||
* bin/gpodder: We're in development again, so add "+svn"
|
||||
* src/gpodder/draw.py: Added
|
||||
* src/gpodder/gui.py: Fix import of renamed "channels_to_model";
|
||||
re-assign new column numbers for treeChannels' data model; add
|
||||
cell renderer for pill pixbuf and remove cell renderer for status text
|
||||
* src/gpodder/libpodcasts.py: Add episode_is_new() function to
|
||||
podcastChannel to check if an episode can be considered "new"; use the
|
||||
episode_is_new function in get_new_episodes; add get_episode_stats()
|
||||
after an idea from Paul Rudkin <paul@thegithouse.com>; re-factor
|
||||
channels_to_model and clean out old, unused code and columns
|
||||
|
||||
Mon, 26 Nov 2007 08:57:04 +0100 <thp@perli.net>
|
||||
gPodder 0.10.2 "Ein schweineschnauzen Sandwich, bitte!" released
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ or played back on the user's desktop.
|
|||
# PLEASE DO NOT CHANGE FORMAT OF __version__ LINE (setup.py reads this)
|
||||
|
||||
__author__ = "Thomas Perl <thp@perli.net>"
|
||||
__version__ = "0.10.2"
|
||||
__version__ = "0.10.2+svn"
|
||||
__date__ = "2007-11-26"
|
||||
__copyright__ = "Copyright (c) 2005-2007 %s. All rights reserved." % __author__
|
||||
__licence__ = "GPL"
|
||||
|
|
149
src/gpodder/draw.py
Normal file
149
src/gpodder/draw.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
|
||||
#
|
||||
# gPodder 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 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gPodder 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, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# draw.py -- Draw routines for gPodder-specific graphics
|
||||
# Thomas Perl <thp@perli.net>, 2007-11-25
|
||||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
import cairo
|
||||
import StringIO
|
||||
|
||||
|
||||
class TextExtents(object):
|
||||
def __init__(self, ctx, text):
|
||||
tuple = ctx.text_extents(text)
|
||||
(self.x_bearing, self.y_bearing, self.width, self.height, self.x_advance, self.y_advance) = tuple
|
||||
|
||||
|
||||
RRECT_LEFT_SIDE = 1
|
||||
RRECT_RIGHT_SIDE = 2
|
||||
|
||||
def draw_rounded_rectangle(ctx, x, y, w, h, r=10, left_side_width = None, sides_to_draw=0):
|
||||
if left_side_width is None:
|
||||
left_side_width = w/2
|
||||
|
||||
if sides_to_draw & RRECT_LEFT_SIDE:
|
||||
ctx.move_to(x+left_side_width, y+h)
|
||||
ctx.line_to(x+r, y+h)
|
||||
ctx.curve_to(x, y+h, x, y+h, x, y+h-r)
|
||||
ctx.line_to(x, y+r)
|
||||
ctx.curve_to(x, y, x, y, x+r, y)
|
||||
ctx.line_to(x+left_side_width, y)
|
||||
|
||||
if sides_to_draw & RRECT_RIGHT_SIDE:
|
||||
ctx.move_to(x+left_side_width, y)
|
||||
ctx.line_to(x+w-r, y)
|
||||
ctx.curve_to(x+w, y, x+w, y, x+w, y+r)
|
||||
ctx.line_to(x+w, y+h-r)
|
||||
ctx.curve_to(x+w, y+h, x+w, y+h, x+w-r, y+h)
|
||||
ctx.line_to(x+left_side_width, y+h)
|
||||
|
||||
|
||||
def set_gtk_color(ctx, col, opacity=1.0, invert=False):
|
||||
# Convert color values 0..65535 -> 0.0..1.0
|
||||
(r, g, b) = map(lambda c: c/65535.0, (col.red, col.green, col.blue))
|
||||
if invert == True:
|
||||
(r, g, b) = (1.0-r, 1.0-g, 1.0-b)
|
||||
ctx.set_source_rgba( r, g, b, opacity)
|
||||
|
||||
|
||||
def draw_text_pill(left_text, right_text, x=0, y=0, border=3, radius=11):
|
||||
# Create temporary context to calculate the text size
|
||||
ctx = cairo.Context(cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
|
||||
|
||||
# Use GTK+ style of a normal Button
|
||||
widget = gtk.ProgressBar()
|
||||
style = widget.rc_get_style()
|
||||
|
||||
bg_color = style.bg[gtk.STATE_SELECTED]
|
||||
text_color = style.text[gtk.STATE_SELECTED]
|
||||
|
||||
font_desc = style.font_desc
|
||||
font_size = float(font_desc.get_size())/float(pango.SCALE)
|
||||
font_name = font_desc.get_family()
|
||||
|
||||
ctx.set_font_size(font_size)
|
||||
ctx.select_font_face(font_name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
||||
|
||||
left_text_e = TextExtents(ctx, left_text)
|
||||
right_text_e = TextExtents(ctx, right_text)
|
||||
text_height = max(left_text_e.height, right_text_e.height)
|
||||
|
||||
image_height = int(y+text_height+border*2)
|
||||
image_width = int(x+left_text_e.width+right_text_e.width+border*4)
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.set_font_size(font_size)
|
||||
ctx.select_font_face(font_name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
||||
|
||||
if left_text == '0':
|
||||
left_text = None
|
||||
if right_text == '0':
|
||||
right_text = None
|
||||
|
||||
left_side_width = left_text_e.width + border*2
|
||||
right_side_width = right_text_e.width + border*2
|
||||
|
||||
rect_width = left_side_width + right_side_width
|
||||
rect_height = text_height + border*2
|
||||
if left_text is not None:
|
||||
draw_rounded_rectangle(ctx,x,y,rect_width,rect_height,radius, left_side_width, RRECT_LEFT_SIDE)
|
||||
set_gtk_color(ctx, bg_color, 0.5)
|
||||
ctx.fill()
|
||||
|
||||
ctx.move_to(x+1+border-left_text_e.x_bearing, y+1+border+text_height)
|
||||
set_gtk_color(ctx, text_color, invert=True)
|
||||
ctx.show_text(left_text)
|
||||
ctx.move_to(x+border-left_text_e.x_bearing, y+border+text_height)
|
||||
set_gtk_color(ctx, text_color)
|
||||
ctx.show_text(left_text)
|
||||
|
||||
if right_text is not None:
|
||||
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE)
|
||||
set_gtk_color(ctx, bg_color)
|
||||
ctx.fill()
|
||||
|
||||
ctx.move_to(x+1+border*3+left_text_e.width-right_text_e.x_bearing, y+1+border+text_height)
|
||||
set_gtk_color(ctx, text_color, invert=True)
|
||||
ctx.show_text(right_text)
|
||||
ctx.move_to(x+border*3+left_text_e.width-right_text_e.x_bearing, y+border+text_height)
|
||||
set_gtk_color(ctx, text_color)
|
||||
ctx.show_text(right_text)
|
||||
|
||||
return surface
|
||||
|
||||
|
||||
def draw_pill_pixbuf(left_text, right_text):
|
||||
s = draw_text_pill(left_text, right_text)
|
||||
sio = StringIO.StringIO()
|
||||
s.write_to_png(sio)
|
||||
|
||||
pbl = gtk.gdk.PixbufLoader()
|
||||
pbl.write(sio.getvalue())
|
||||
pbl.close()
|
||||
|
||||
pixbuf = pbl.get_pixbuf()
|
||||
return pixbuf
|
||||
|
|
@ -39,7 +39,7 @@ from gpodder import download
|
|||
from gpodder import SimpleGladeApp
|
||||
|
||||
from libpodcasts import podcastChannel
|
||||
from libpodcasts import channelsToModel
|
||||
from libpodcasts import channels_to_model
|
||||
from libpodcasts import load_channels
|
||||
from libpodcasts import save_channels
|
||||
|
||||
|
@ -180,18 +180,17 @@ class gPodder(GladeWidget):
|
|||
|
||||
iconcell = gtk.CellRendererPixbuf()
|
||||
namecolumn.pack_start( iconcell, False)
|
||||
namecolumn.add_attribute( iconcell, 'pixbuf', 8)
|
||||
namecolumn.add_attribute( iconcell, 'pixbuf', 5)
|
||||
|
||||
namecell = gtk.CellRendererText()
|
||||
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
namecolumn.pack_start( namecell, True)
|
||||
namecolumn.add_attribute( namecell, 'markup', 7)
|
||||
namecolumn.add_attribute( namecell, 'markup', 2)
|
||||
namecolumn.add_attribute( namecell, 'weight', 4)
|
||||
|
||||
newcell = gtk.CellRendererText()
|
||||
namecolumn.pack_end( newcell, False)
|
||||
namecolumn.add_attribute( newcell, 'text', 5)
|
||||
namecolumn.add_attribute( newcell, 'weight', 4)
|
||||
iconcell = gtk.CellRendererPixbuf()
|
||||
namecolumn.pack_start( iconcell, False)
|
||||
namecolumn.add_attribute( iconcell, 'pixbuf', 3)
|
||||
|
||||
self.treeChannels.append_column( namecolumn)
|
||||
|
||||
|
@ -579,7 +578,7 @@ class gPodder(GladeWidget):
|
|||
selected = (0,)
|
||||
|
||||
rect = self.treeChannels.get_visible_rect()
|
||||
self.treeChannels.set_model( channelsToModel( self.channels))
|
||||
self.treeChannels.set_model(channels_to_model(self.channels))
|
||||
self.treeChannels.scroll_to_point( rect.x, rect.y)
|
||||
while gtk.events_pending():
|
||||
gtk.main_iteration( False)
|
||||
|
|
|
@ -36,7 +36,7 @@ from gpodder import util
|
|||
from gpodder import opml
|
||||
from gpodder import cache
|
||||
from gpodder import services
|
||||
|
||||
from gpodder import draw
|
||||
|
||||
from liblogger import log
|
||||
import libgpodder
|
||||
|
@ -266,7 +266,26 @@ class podcastChannel(ListType):
|
|||
for episode in self.load_downloaded_episodes():
|
||||
pubdate = episode.newer_pubdate( pubdate)
|
||||
return pubdate
|
||||
|
||||
def episode_is_new(self, episode, last_pubdate = None):
|
||||
gl = libgpodder.gPodderLib()
|
||||
if last_pubdate is None:
|
||||
last_pubdate = self.newest_pubdate_downloaded()
|
||||
|
||||
# episode is older than newest downloaded
|
||||
if episode.compare_pubdate(last_pubdate) < 0:
|
||||
return False
|
||||
|
||||
# episode has been downloaded before
|
||||
if episode.is_downloaded() or gl.history_is_downloaded(episode.url):
|
||||
return False
|
||||
|
||||
# download is currently in progress
|
||||
if services.download_status_manager.is_download_in_progress(episode.url):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_new_episodes( self):
|
||||
last_pubdate = self.newest_pubdate_downloaded()
|
||||
gl = libgpodder.gPodderLib()
|
||||
|
@ -275,21 +294,9 @@ class podcastChannel(ListType):
|
|||
return self[0:min(len(self),gl.config.default_new)]
|
||||
|
||||
new_episodes = []
|
||||
|
||||
for episode in self.get_all_episodes():
|
||||
# episode is older than newest downloaded
|
||||
if episode.compare_pubdate( last_pubdate) < 0:
|
||||
continue
|
||||
|
||||
# episode has been downloaded before
|
||||
if episode.is_downloaded() or gl.history_is_downloaded( episode.url):
|
||||
continue
|
||||
|
||||
# download is currently in progress
|
||||
if services.download_status_manager.is_download_in_progress( episode.url):
|
||||
continue
|
||||
|
||||
new_episodes.append( episode)
|
||||
if self.episode_is_new(episode, last_pubdate):
|
||||
new_episodes.append(episode)
|
||||
|
||||
return new_episodes
|
||||
|
||||
|
@ -361,7 +368,24 @@ class podcastChannel(ListType):
|
|||
episodes.sort( reverse = True)
|
||||
|
||||
return episodes
|
||||
|
||||
|
||||
def get_episode_stats( self):
|
||||
(available, downloaded, newer, unplayed) = (0, 0, 0, 0)
|
||||
last_pubdate = self.newest_pubdate_downloaded()
|
||||
|
||||
for episode in self.get_all_episodes():
|
||||
available += 1
|
||||
if self.episode_is_new(episode, last_pubdate):
|
||||
newer += 1
|
||||
if episode.is_downloaded():
|
||||
downloaded += 1
|
||||
if not self.is_played(episode):
|
||||
unplayed += 1
|
||||
|
||||
return (available, downloaded, newer, unplayed)
|
||||
|
||||
|
||||
def force_update_tree_model( self):
|
||||
self.__tree_model = None
|
||||
|
||||
|
@ -656,42 +680,32 @@ class podcastItem(object):
|
|||
|
||||
|
||||
|
||||
def channelsToModel( channels):
|
||||
new_model = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, gtk.gdk.Pixbuf)
|
||||
pos = 0
|
||||
def channels_to_model(channels):
|
||||
new_model = gtk.ListStore(str, str, str, gtk.gdk.Pixbuf, int, gtk.gdk.Pixbuf)
|
||||
|
||||
for channel in channels:
|
||||
new_episodes = channel.get_new_episodes()
|
||||
count = len(channel)
|
||||
count_new = len(new_episodes)
|
||||
|
||||
(count_available, count_downloaded, count_new, count_unplayed) = channel.get_episode_stats()
|
||||
|
||||
new_iter = new_model.append()
|
||||
new_model.set( new_iter, 0, channel.url)
|
||||
new_model.set( new_iter, 1, channel.title)
|
||||
new_model.set(new_iter, 0, channel.url)
|
||||
new_model.set(new_iter, 1, channel.title)
|
||||
|
||||
new_model.set( new_iter, 2, count)
|
||||
if count_new == 0:
|
||||
new_model.set( new_iter, 3, '')
|
||||
elif count_new == 1:
|
||||
new_model.set( new_iter, 3, _('New episode: %s') % ( new_episodes[-1].title ) + ' ')
|
||||
else:
|
||||
new_model.set( new_iter, 3, _('%s new episodes') % count_new + ' ')
|
||||
title_markup = saxutils.escape(channel.title)
|
||||
description_markup = saxutils.escape(util.get_first_line(channel.description))
|
||||
new_model.set(new_iter, 2, '%s\n<small>%s</small>' % (title_markup, description_markup))
|
||||
|
||||
if count_new:
|
||||
if count_unplayed > 0 or count_downloaded > 0:
|
||||
new_model.set(new_iter, 3, draw.draw_pill_pixbuf(str(count_unplayed), str(count_downloaded)))
|
||||
|
||||
if count_new > 0:
|
||||
new_model.set( new_iter, 4, pango.WEIGHT_BOLD)
|
||||
new_model.set( new_iter, 5, str(count_new))
|
||||
else:
|
||||
new_model.set( new_iter, 4, pango.WEIGHT_NORMAL)
|
||||
new_model.set( new_iter, 5, '')
|
||||
|
||||
new_model.set( new_iter, 6, pos)
|
||||
|
||||
new_model.set( new_iter, 7, '%s\n<small>%s</small>' % ( saxutils.escape( channel.title), saxutils.escape( channel.description.split('\n')[0]), ))
|
||||
|
||||
channel_cover_found = False
|
||||
if os.path.exists( channel.cover_file) and os.path.getsize(channel.cover_file) > 0:
|
||||
try:
|
||||
new_model.set( new_iter, 8, gtk.gdk.pixbuf_new_from_file_at_size( channel.cover_file, 32, 32))
|
||||
new_model.set( new_iter, 5, gtk.gdk.pixbuf_new_from_file_at_size( channel.cover_file, 32, 32))
|
||||
channel_cover_found = True
|
||||
except:
|
||||
exctype, value = sys.exc_info()[:2]
|
||||
|
@ -704,12 +718,10 @@ def channelsToModel( channels):
|
|||
icon_theme = gtk.icon_theme_get_default()
|
||||
globe_icon_name = 'applications-internet'
|
||||
try:
|
||||
new_model.set( new_iter, 8, icon_theme.load_icon(globe_icon_name, iconsize, 0))
|
||||
new_model.set( new_iter, 5, icon_theme.load_icon(globe_icon_name, iconsize, 0))
|
||||
except:
|
||||
log( 'Cannot load "%s" icon (using an old or incomplete icon theme?)', globe_icon_name)
|
||||
new_model.set( new_iter, 8, None)
|
||||
|
||||
pos = pos + 1
|
||||
new_model.set( new_iter, 5, None)
|
||||
|
||||
return new_model
|
||||
|
||||
|
|
Loading…
Reference in a new issue