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:
Thomas Perl 2007-11-27 22:04:15 +00:00
parent e8b8e1bd0c
commit 1bff3e1227
5 changed files with 226 additions and 52 deletions

View file

@ -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

View file

@ -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
View 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

View file

@ -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)

View file

@ -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