diff --git a/resources/images/jobs-animated.mng b/resources/images/jobs-animated.mng deleted file mode 100644 index affe3b69fd..0000000000 Binary files a/resources/images/jobs-animated.mng and /dev/null differ diff --git a/resources/jquery.simulate.js b/resources/jquery.simulate.js new file mode 100644 index 0000000000..d52140bb86 --- /dev/null +++ b/resources/jquery.simulate.js @@ -0,0 +1,152 @@ +/* + * jquery.simulate - simulate browser mouse and keyboard events + * + * Copyright (c) 2009 Eduardo Lundgren (eduardolundgren@gmail.com) + * and Richard D. Worth (rdworth@gmail.com) + * + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + */ + +;(function($) { + +$.fn.extend({ + simulate: function(type, options) { + return this.each(function() { + var opt = $.extend({}, $.simulate.defaults, options || {}); + new $.simulate(this, type, opt); + }); + } +}); + +$.simulate = function(el, type, options) { + this.target = el; + this.options = options; + + if (/^drag$/.test(type)) { + this[type].apply(this, [this.target, options]); + } else { + this.simulateEvent(el, type, options); + } +} + +$.extend($.simulate.prototype, { + simulateEvent: function(el, type, options) { + var evt = this.createEvent(type, options); + this.dispatchEvent(el, type, evt, options); + return evt; + }, + createEvent: function(type, options) { + if (/^mouse(over|out|down|up|move)|(dbl)?click$/.test(type)) { + return this.mouseEvent(type, options); + } else if (/^key(up|down|press)$/.test(type)) { + return this.keyboardEvent(type, options); + } + }, + mouseEvent: function(type, options) { + var evt; + var e = $.extend({ + bubbles: true, cancelable: (type != "mousemove"), view: window, detail: 0, + screenX: 0, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 0, relatedTarget: undefined + }, options); + + var relatedTarget = $(e.relatedTarget)[0]; + + if ($.isFunction(document.createEvent)) { + evt = document.createEvent("MouseEvents"); + evt.initMouseEvent(type, e.bubbles, e.cancelable, e.view, e.detail, + e.screenX, e.screenY, e.clientX, e.clientY, + e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, + e.button, e.relatedTarget || document.body.parentNode); + } else if (document.createEventObject) { + evt = document.createEventObject(); + $.extend(evt, e); + evt.button = { 0:1, 1:4, 2:2 }[evt.button] || evt.button; + } + return evt; + }, + keyboardEvent: function(type, options) { + var evt; + + var e = $.extend({ bubbles: true, cancelable: true, view: window, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0, charCode: 0 + }, options); + + if ($.isFunction(document.createEvent)) { + try { + evt = document.createEvent("KeyEvents"); + evt.initKeyEvent(type, e.bubbles, e.cancelable, e.view, + e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, + e.keyCode, e.charCode); + } catch(err) { + evt = document.createEvent("Events"); + evt.initEvent(type, e.bubbles, e.cancelable); + $.extend(evt, { view: e.view, + ctrlKey: e.ctrlKey, altKey: e.altKey, shiftKey: e.shiftKey, metaKey: e.metaKey, + keyCode: e.keyCode, charCode: e.charCode + }); + } + } else if (document.createEventObject) { + evt = document.createEventObject(); + $.extend(evt, e); + } + if ($.browser.msie || $.browser.opera) { + evt.keyCode = (e.charCode > 0) ? e.charCode : e.keyCode; + evt.charCode = undefined; + } + return evt; + }, + + dispatchEvent: function(el, type, evt) { + if (el.dispatchEvent) { + el.dispatchEvent(evt); + } else if (el.fireEvent) { + el.fireEvent('on' + type, evt); + } + return evt; + }, + + drag: function(el) { + var self = this, center = this.findCenter(this.target), + options = this.options, x = Math.floor(center.x), y = Math.floor(center.y), + dx = options.dx || 0, dy = options.dy || 0, target = this.target; + var coord = { clientX: x, clientY: y }; + this.simulateEvent(target, "mousedown", coord); + coord = { clientX: x + 1, clientY: y + 1 }; + this.simulateEvent(document, "mousemove", coord); + coord = { clientX: x + dx, clientY: y + dy }; + this.simulateEvent(document, "mousemove", coord); + this.simulateEvent(document, "mousemove", coord); + this.simulateEvent(target, "mouseup", coord); + }, + findCenter: function(el) { + var el = $(this.target), o = el.offset(); + return { + x: o.left + el.outerWidth() / 2, + y: o.top + el.outerHeight() / 2 + }; + } +}); + +$.extend($.simulate, { + defaults: { + speed: 'sync' + }, + VK_TAB: 9, + VK_ENTER: 13, + VK_ESC: 27, + VK_PGUP: 33, + VK_PGDN: 34, + VK_END: 35, + VK_HOME: 36, + VK_LEFT: 37, + VK_UP: 38, + VK_RIGHT: 39, + VK_DOWN: 40 +}); + +})(jQuery); diff --git a/resources/recipes/bangkokpost.recipe b/resources/recipes/bangkokpost.recipe new file mode 100644 index 0000000000..fc4d60c40e --- /dev/null +++ b/resources/recipes/bangkokpost.recipe @@ -0,0 +1,45 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class BangkokPostRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = 'kwetal' + language = 'en_TH' + version = 1 + + title = u'Bangkok Post' + publisher = u'Post Publishing PCL' + category = u'News' + description = u'The world\'s window to Thailand' + + oldest_article = 7 + max_articles_per_feed = 100 + + no_stylesheets = True + remove_javascript = True + use_embedded_content = False + + # Feeds from: http://www.bangkokpost.com/rss/ + feeds = [] + feeds.append((u'Breaking News', u'http://www.bangkokpost.com/rss/data/breakingnews.xml')) + feeds.append((u'Top Stories', u'http://www.bangkokpost.com/rss/data/topstories.xml')) + feeds.append((u'News', u'http://www.bangkokpost.com/rss/data/news.xml')) + feeds.append((u'Business', u'http://www.bangkokpost.com/rss/data/business.xml')) + feeds.append((u'Opinion', u'http://www.bangkokpost.com/rss/data/opinion.xml')) + feeds.append((u'Travel', u'http://www.bangkokpost.com/rss/data/travel.xml')) + feeds.append((u'Leisure', u'http://www.bangkokpost.com/rss/data/leisure.xml')) + feeds.append((u'Entertainment', u'http://www.bangkokpost.com/rss/data/entertainment.xml')) + feeds.append((u'Auto', u'http://www.bangkokpost.com/rss/data/auto.xml')) + feeds.append((u'Life', u'http://www.bangkokpost.com/rss/data/life.xml')) + feeds.append((u'Tech', u'http://www.bangkokpost.com/rss/data/tect.xml')) + + keep_only_tags = [] + keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'entry'})) + + remove_tags = [] + remove_tags.append(dict(name = 'div', attrs = {'class': 'article-features'})) + remove_tags.append(dict(name = 'div', attrs = {'class': 'socialBookmark'})) + remove_tags.append(dict(name = 'div', attrs = {'id': 'main-sns'})) + # Their YouTube movies are displayed in an iframe, if you want those you will have to parse the articles by hand. + # Setting self.recursion to 1, which might resolve this, makes calibre downloading a lot of PDF files, which will cause a very, very very, long download time + remove_tags.append(dict(name = 'iframe')) + diff --git a/resources/recipes/glennbeck.recipe b/resources/recipes/glennbeck.recipe new file mode 100644 index 0000000000..09f54b2d6f --- /dev/null +++ b/resources/recipes/glennbeck.recipe @@ -0,0 +1,97 @@ +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, Comment + +class GlennBeckRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = 'kwetal' + language = 'en' + version = 1 + + title = u'Glenn Beck' + publisher = u'Premiere Radio Networks' + category = u'News, Opinion' + description = u'The fusion of entertainment and enlightenment' + + oldest_article = 7 + max_articles_per_feed = 100 + + no_stylesheets = True + remove_javascript = True + use_embedded_content = False + + feeds = [(u'Glenn Beck', u'http://feeds.feedburner.com/GlennBeckArticles')] + + def preprocess_html(self, soup): + # Their html is horribly broken; if we search for the div that has the content BeatifulSoup returns the div with only the headline and no content. + # This is due to illegal nesting of tags. So we do it the hard way. + + # We can find this one, and we don't want it. + div = soup.find('div', attrs = {'id': 'extraInfo'}) + if div: + div.extract() + + # Don't want these either. + iframes = soup.findAll('iframe') + [iframe.extract() for iframe in iframes] + + # Get empty document. + freshSoup = self.getFreshSoup() + + # This is the broken div; but we can find the headline. + newsDiv = soup.find('div', attrs = {'class': 'news-detail'}) + if newsDiv: + if newsDiv.h1: + freshSoup.body.append(newsDiv.h1) + + # The content is wrapped in

tags, most of the time anyway. + counter = 0 + for p in soup.findAll('p'): + if p.get('class') == 'smalltextwhite': + # But we don't want this one. + continue + + freshSoup.body.append(p) + counter += 1 + + # Debugging block + #h3 = Tag(freshSoup, 'h3') + #h3.append('First counter: ' + str(counter)) + #freshSoup.body.insert(0, h3) + + # In some articles the content is not wrapped in

tags. In that case the counter is low. + # 2 is the magic number that seems to work. + if counter <= 2: + # So they are playing hard-to-get: first throw out all comments. + comments = soup.findAll(text = lambda text: isinstance(text, Comment)) + [comment.extract() for comment in comments] + + # Find all unwrapped strings. + for txt in soup.findAll(text = True): + raw = txt.strip() + # Debugging line + #para.append(raw + '(parent: ' + txt.parent.name + '; length: ' + str(len(raw)) + '; start: ' + raw[0:4] + ')') + + if (txt.parent.name == 'body' and len(raw) > 0) and not (len(raw) == 6 and raw == ' '): + # This is our content; ignore the rest. + para = Tag(freshSoup, 'p') + para.append(raw) + freshSoup.body.append(para) + counter += 1 + + # Now if the counter is still 0 or 1 they did something completely different and we still have an empty article. In a last attempt, add the whole content div, just in case. + if counter < 2: + freshSoup.body.append(newsDiv) + + # Debugging block + #h3 = Tag(freshSoup, 'h3') + #h3.append('Second counter: ' + str(counter)) + #freshSoup.body.insert(1, h3) + + return freshSoup + + def getFreshSoup(self, title = None): + if title: + return BeautifulSoup('' + str(title) + '') + else: + return BeautifulSoup('') + diff --git a/resources/recipes/huffingtonpost.recipe b/resources/recipes/huffingtonpost.recipe new file mode 100644 index 0000000000..fc5ba26f1c --- /dev/null +++ b/resources/recipes/huffingtonpost.recipe @@ -0,0 +1,134 @@ +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, Comment +import re + +class HuffingtonPostRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = 'kwetal' + language = 'en' + version = 1 + + title = u'The Huffington Post' + publisher = u'huffingtonpost.com' + category = u'News, Politics' + description = u'Political Blog' + + oldest_article = 1.5 + max_articles_per_feed = 100 + use_embedded_content = False + + no_stylesheets = True + remove_javascript = True + # Seems to work best, but YMMV + simultaneous_downloads = 1 + + feeds = [] + feeds.append((u'Latest News', u'http://feeds.huffingtonpost.com/huffingtonpost/LatestNews')) + # Works, but appears to be a subset of the politics-blog feed + #feeds.append((u'Politics', u'http://www.huffingtonpost.com/feeds/verticals/politics/index.xml')) + feeds.append((u'Politics', u'http://www.huffingtonpost.com/feeds/verticals/politics/blog.xml')) + # Does not work + #feeds.append((u'Politics: News', u'http://www.huffingtonpost.com/feeds/verticals/politics/news.xml')) + # Works, but appears to be a subset of the media-blog feed + #feeds.append((u'Media', u'http://www.huffingtonpost.com/feeds/verticals/media/index.xml')) + feeds.append((u'Media', u'http://www.huffingtonpost.com/feeds/verticals/media/blog.xml')) + # Does not work + #feeds.append((u'Media: News', u'http://www.huffingtonpost.com/feeds/verticals/media/news.xml')) + # Works, but appears to be a subset of the business-blog feed + #feeds.append((u'Business', u'http://www.huffingtonpost.com/feeds/verticals/business/index.xml')) + feeds.append((u'Business', u'http://www.huffingtonpost.com/feeds/verticals/business/blog.xml')) + # Does not work + #feeds.append((u'Business: News', u'http://www.huffingtonpost.com/feeds/verticals/business/news.xml')) + feeds.append((u'Original Reporting', u'http://www.huffingtonpost.com/tag/huffpolitics/feed')) + feeds.append((u'Original Posts', u'http://www.huffingtonpost.com/feeds/original_posts/index.xml')) + + keep_only_tags = [] + # For reporters posts + keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'about_reporter_name'})) + keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'entry'})) + # For blog posts + keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'blog_author_info'})) + keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'blog_title'})) + + remove_tags = [] + remove_tags.append(dict(name = 'div', attrs = {'class' : 'contin_below'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'adver_cont_below'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'blogger_menu_content'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'chicklets'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'google-searcG-blogp'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'forma_email'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'new_share_module'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'cse-branding-right'})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'clear'})) + remove_tags.append(dict(name = 'div', attrs = {'style' : re.compile('clear:both;*')})) + remove_tags.append(dict(name = 'div', attrs = {'class' : re.compile('facebookvote_reaction_blog.*')})) + remove_tags.append(dict(name = 'div', attrs = {'class' : re.compile('read_more.*')})) + remove_tags.append(dict(name = 'div', attrs = {'class' : re.compile('facebookvote_v2.*')})) + remove_tags.append(dict(name = 'div', attrs = {'class' : re.compile('facebookvote_reaction.*')})) + remove_tags.append(dict(name = 'div', attrs = {'class' : 'slideshow_poll'})) + + remove_attributes = ['style'] + + extra_css = 'a[href] {color: blue; text-decoration: none; cursor: pointer;}' + + def preprocess_html(self, soup): + # Condens the soup. + soup = self.unComment(self.nukeHead(soup)) + + # Don't want the picture of the author + blogAuthor = soup.find('div', attrs = {'id': 'blog_author_info'}) + if blogAuthor: + for img in blogAuthor.findAll('img'): + img.extract() + + byline = soup.find('h2') + if byline: + h2 = Tag(soup, 'h2') + raw = self.tag_to_string(byline) + h2.append(raw) + byline.replaceWith(h2) + else: + byline = soup.find('div', attrs = {'class': re.compile('about_*reporter_*name')}) + if byline: + h2 = Tag(soup, 'h2') + raw = self.tag_to_string(byline) + h2.append(raw.strip()) + byline.replaceWith(h2) + + headline = soup.find('h1') + if headline: + h1 = Tag(soup, 'h1') + raw = self.tag_to_string(headline) + h1.append(raw) + headline.replaceWith(h1) + + return soup + + def postprocess_html(self, soup, first): + # Get rid of those pesky
tags + html = re.sub(r'\n
\n', '', str(soup)) + newSoup = BeautifulSoup(html) + + return newSoup + + def nukeHead(self, soup): + titleStr = '' + newHead = Tag(soup, 'head') + newTitle = Tag(soup, 'title') + newHead.append(newTitle) + head = soup.head + if head: + title = head.title + if title: + titleStr = self.tag_to_string(title) + newTitle.append(titleStr) + head.replaceWith(newHead) + else: + soup.insert(0, newHead) + return soup + + def unComment(self, soup): + comments = soup.findAll(text = lambda text: isinstance(text, Comment)) + [comment.extract() for comment in comments] + return soup + diff --git a/resources/recipes/rian_eng.recipe b/resources/recipes/rian_eng.recipe new file mode 100644 index 0000000000..172a50beda --- /dev/null +++ b/resources/recipes/rian_eng.recipe @@ -0,0 +1,42 @@ + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +en.rian.ru +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Ria_eng(BasicNewsRecipe): + title = 'Ria Novosti' + __author__ = 'Darko Miletic' + description = 'News from Russia in English' + language = 'en' + publisher = 'en.rian.ru' + category = 'news, politics, Russia' + oldest_article = 3 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + encoding = 'utf-8' + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + + keep_only_tags = [dict(name='div', attrs={'class':'article'})] + + remove_tags = [ + dict(name=['object','link','iframe','base']) + ,dict(name='div',attrs={'class':['related','mmban','view-story']}) + ,dict(name='span',attrs={'class':'copyright'}) + ] + remove_tags_after = dict(name='div',attrs={'class':'text'}) + + + feeds = [(u'Online news', u'http://en.rian.ru/export/rss2/archive/index.xml')] + diff --git a/resources/recipes/rian_spa.recipe b/resources/recipes/rian_spa.recipe new file mode 100644 index 0000000000..5d2115168b --- /dev/null +++ b/resources/recipes/rian_spa.recipe @@ -0,0 +1,41 @@ + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +sp.rian.ru +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Ria_eng(BasicNewsRecipe): + title = 'Ria Novosti' + __author__ = 'Darko Miletic' + description = 'Noticias desde Russia en Castellano' + language = 'es' + publisher = 'sp.rian.ru' + category = 'news, politics, Russia' + oldest_article = 3 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + encoding = 'utf-8' + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + + keep_only_tags = [dict(name='div', attrs={'class':'articletxt'})] + remove_tags = [dict(name=['object','link','iframe','base'])] + remove_tags_after = dict(name='div',attrs={'class':'text'}) + + + feeds = [(u'Noticias', u'http://sp.rian.ru/export/rss2/index.xml')] + + def print_version(self, url): + return url.replace('.html','-print.html') + + diff --git a/resources/recipes/sabah.recipe b/resources/recipes/sabah.recipe new file mode 100644 index 0000000000..bc43d52c04 --- /dev/null +++ b/resources/recipes/sabah.recipe @@ -0,0 +1,33 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1253477125(BasicNewsRecipe): + title = u'Sabah' + __author__ = u'Deniz O\u011fuz' + language = 'tr' + oldest_article = 1 + max_articles_per_feed = 20 + cover_url = 'http://www.sabah.com.tr/c/sb/i/sabah_logo.gif' + no_stylesheets = True + remove_tags = [dict(name='div', attrs={'style':['width: 200px', 'border-top: #d8d8d8 1px dotted', 'width: 200px; margin-right: 5px', 'padding-top: 1px']})] + keep_only_tags = [dict(name='div', attrs={'class':['haber line_height_def', 'haber haber_renk line_height_def']})] + extra_css = ''' + body{font-family:Arial,Helvetica,sans-serif; font-size:small; align:left} + h1{font-size:large;} + .sh{font-size:large; font-weight:bold} + .cap{font-size:xx-small; } + .lu{font-size:xx-small; } + .ds{font-size:xx-small; } + .mvb{font-size:xx-small;} + .by1{font-size:x-small; color:#666666} + .byd{font-size:x-small;} + ''' + feeds = [(u'Son 24 Saat', u'http://www.sabah.com.tr/rss/Son24Saat.xml'), + (u'Ekonomi', u'http://www.sabah.com.tr/rss/Ekonomi.xml'), + (u'G\xfcndem', u'http://www.sabah.com.tr/rss/Gundem.xml'), + (u'Siyaset', u'http://www.sabah.com.tr/rss/Siyaset.xml'), + (u'Yazarlar', u'http://www.sabah.com.tr/rss/Yazarlar.xml'), + (u'D\xfcnya', u'http://www.sabah.com.tr/rss/Dunya.xml'), + (u'Teknoloji', u'http://www.sabah.com.tr/rss/Teknoloji.xml'), + (u'Spor', u'http://www.sabah.com.tr/rss/Spor.xml'), + (u'G\xfcn\xfcn \u0130\xe7inden', u'http://www.sabah.com.tr/rss/gununicinden.xml'), + (u'Emlak', u'http://www.sabah.com.tr/rss/Emlak.xml'),] diff --git a/resources/recipes/soldiers.recipe b/resources/recipes/soldiers.recipe index 173ab49925..fb96e5a2ed 100644 --- a/resources/recipes/soldiers.recipe +++ b/resources/recipes/soldiers.recipe @@ -1,4 +1,3 @@ -#!/usr/bin/env python __license__ = 'GPL v3' __copyright__ = '2009, Darko Miletic ' @@ -16,26 +15,23 @@ class Soldiers(BasicNewsRecipe): max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False - remove_javascript = True simultaneous_downloads = 1 delay = 4 max_connections = 1 encoding = 'utf-8' publisher = 'U.S. Army' category = 'news, politics, war, weapons' - language = 'en' - + language = 'en' INDEX = 'http://www.army.mil/soldiers/' - html2lrf_options = [ - '--comment', description - , '--category', category - , '--publisher', publisher - ] - - html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } - keep_only_tags = [dict(name='div', attrs={'id':'rightCol'})] + keep_only_tags = [dict(name='div', attrs={'id':['storyHeader','textArea']})] remove_tags = [ dict(name='div', attrs={'id':['addThis','comment','articleFooter']}) @@ -44,10 +40,6 @@ class Soldiers(BasicNewsRecipe): feeds = [(u'Frontpage', u'http://www.army.mil/rss/feeds/soldiersfrontpage.xml' )] - def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - return soup def get_cover_url(self): cover_url = None @@ -56,3 +48,4 @@ class Soldiers(BasicNewsRecipe): if cover_item: cover_url = cover_item['src'] return cover_url + diff --git a/resources/recipes/zaman.recipe b/resources/recipes/zaman.recipe new file mode 100644 index 0000000000..064b2c265a --- /dev/null +++ b/resources/recipes/zaman.recipe @@ -0,0 +1,20 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class ZamanRecipe(BasicNewsRecipe): + title = u'Zaman' + __author__ = u'Deniz Og\xfcz' + language = 'tr' + oldest_article = 1 + max_articles_per_feed = 10 + + cover_url = 'http://medya.zaman.com.tr/zamantryeni/pics/zamanonline.gif' + feeds = [(u'Gundem', u'http://www.zaman.com.tr/gundem.rss'), + (u'Son Dakika', u'http://www.zaman.com.tr/sondakika.rss'), + (u'Spor', u'http://www.zaman.com.tr/spor.rss'), + (u'Ekonomi', u'http://www.zaman.com.tr/ekonomi.rss'), + (u'Politika', u'http://www.zaman.com.tr/politika.rss'), + (u'D\u0131\u015f Haberler', u'http://www.zaman.com.tr/dishaberler.rss'), + (u'Yazarlar', u'http://www.zaman.com.tr/yazarlar.rss'),] + + def print_version(self, url): + return url.replace('www.zaman.com.tr/haber.do?', 'www.zaman.com.tr/yazdir.do?') diff --git a/setup/extensions.py b/setup/extensions.py index 7c8e9f9e78..533378c3d0 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -112,6 +112,13 @@ extensions = [ sip_files = ['calibre/gui2/pictureflow/pictureflow.sip'] ), + Extension('progress_indicator', + ['calibre/gui2/progress_indicator/QProgressIndicator.cpp'], + inc_dirs = ['calibre/gui2/progress_indicator'], + headers = ['calibre/gui2/progress_indicator/QProgressIndicator.h'], + sip_files = ['calibre/gui2/progress_indicator/QProgressIndicator.sip'] + ), + ] diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 489fbc2982..2aa6cb6ed5 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -55,7 +55,7 @@ if plugins is None: sys.path.insert(0, plugin_path) for plugin in ['pictureflow', 'lzx', 'msdes', 'podofo', 'cPalmdoc', - 'fontconfig', 'pdfreflow'] + \ + 'fontconfig', 'pdfreflow', 'progress_indicator'] + \ (['winutil'] if iswindows else []) + \ (['usbobserver'] if isosx else []): try: diff --git a/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp b/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp new file mode 100644 index 0000000000..ce4e4364ad --- /dev/null +++ b/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp @@ -0,0 +1,124 @@ +#include "QProgressIndicator.h" + +#include + +QProgressIndicator::QProgressIndicator(QWidget* parent, int size) + : QWidget(parent), + m_angle(0), + m_timerId(-1), + m_delay(80), + m_displayedWhenStopped(true), + m_displaySize(size), + m_color(Qt::black) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setFocusPolicy(Qt::NoFocus); +} + +bool QProgressIndicator::isAnimated () const +{ + return (m_timerId != -1); +} + +void QProgressIndicator::setDisplayedWhenStopped(bool state) +{ + m_displayedWhenStopped = state; + + update(); +} + +void QProgressIndicator::setDisplaySize(int size) +{ + m_displaySize = size; + update(); +} + + +bool QProgressIndicator::isDisplayedWhenStopped() const +{ + return m_displayedWhenStopped; +} + +void QProgressIndicator::startAnimation() +{ + m_angle = 0; + + if (m_timerId == -1) + m_timerId = startTimer(m_delay); +} + +void QProgressIndicator::stopAnimation() +{ + if (m_timerId != -1) + killTimer(m_timerId); + + m_timerId = -1; + + update(); +} + +void QProgressIndicator::setAnimationDelay(int delay) +{ + if (m_timerId != -1) + killTimer(m_timerId); + + m_delay = delay; + + if (m_timerId != -1) + m_timerId = startTimer(m_delay); +} + +void QProgressIndicator::setColor(const QColor & color) +{ + m_color = color; + + update(); +} + +QSize QProgressIndicator::sizeHint() const +{ + return QSize(m_displaySize, m_displaySize); +} + +int QProgressIndicator::heightForWidth(int w) const +{ + return w; +} + +void QProgressIndicator::timerEvent(QTimerEvent * /*event*/) +{ + m_angle = (m_angle+30)%360; + + update(); +} + +void QProgressIndicator::paintEvent(QPaintEvent * /*event*/) +{ + if (!m_displayedWhenStopped && !isAnimated()) + return; + + int width = qMin(this->width(), this->height()); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + int outerRadius = (width-1)*0.5; + int innerRadius = (width-1)*0.5*0.38; + + int capsuleHeight = outerRadius - innerRadius; + int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35; + int capsuleRadius = capsuleWidth/2; + + for (int i=0; i<12; i++) + { + QColor color = m_color; + color.setAlphaF(1.0f - (i/12.0f)); + p.setPen(Qt::NoPen); + p.setBrush(color); + p.save(); + p.translate(rect().center()); + p.rotate(m_angle - i*30.0f); + p.drawRoundedRect(-capsuleWidth*0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius); + p.restore(); + } +} diff --git a/src/calibre/gui2/progress_indicator/QProgressIndicator.h b/src/calibre/gui2/progress_indicator/QProgressIndicator.h new file mode 100644 index 0000000000..e7e84cf95a --- /dev/null +++ b/src/calibre/gui2/progress_indicator/QProgressIndicator.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +/*! + \class QProgressIndicator + \brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way. + + Progress indicators are indeterminate and do nothing more than spin to show that the application is busy. + \sa QProgressBar +*/ +class QProgressIndicator : public QWidget +{ + Q_OBJECT + Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay) + Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped) + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(int displaySize READ displaySize WRITE setDisplaySize) +public: + QProgressIndicator(QWidget* parent = 0, int size = 64); + + /*! Returns the delay between animation steps. + \return The number of milliseconds between animation steps. By default, the animation delay is set to 80 milliseconds. + \sa setAnimationDelay + */ + int animationDelay() const { return m_delay; } + + /*! Returns a Boolean value indicating whether the component is currently animated. + \return Animation state. + \sa startAnimation stopAnimation + */ + bool isAnimated () const; + + /*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating. + \return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false. + \sa setDisplayedWhenStopped + */ + bool isDisplayedWhenStopped() const; + + /*! Returns the color of the component. + \sa setColor + */ + const QColor & color() const { return m_color; } + + virtual QSize sizeHint() const; + int heightForWidth(int w) const; + int displaySize() const { return m_displaySize; } +public slots: + /*! Starts the spin animation. + \sa stopAnimation isAnimated + */ + void startAnimation(); + + /*! Stops the spin animation. + \sa startAnimation isAnimated + */ + void stopAnimation(); + + /*! Sets the delay between animation steps. + Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up. + \param delay The delay, in milliseconds. + \sa animationDelay + */ + void setAnimationDelay(int delay); + + /*! Sets whether the component hides itself when it is not animating. + \param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true. + \sa isDisplayedWhenStopped + */ + void setDisplayedWhenStopped(bool state); + + /*! Sets the color of the components to the given color. + \sa color + */ + void setColor(const QColor & color); + + /*! Set the size of this widget (used by sizeHint) + * \sa displaySize + */ + void setDisplaySize(int size); +protected: + virtual void timerEvent(QTimerEvent * event); + virtual void paintEvent(QPaintEvent * event); +private: + int m_angle; + int m_timerId; + int m_delay; + int m_displaySize; + bool m_displayedWhenStopped; + QColor m_color; +}; + diff --git a/src/calibre/gui2/progress_indicator/QProgressIndicator.sip b/src/calibre/gui2/progress_indicator/QProgressIndicator.sip new file mode 100644 index 0000000000..d51e6a438b --- /dev/null +++ b/src/calibre/gui2/progress_indicator/QProgressIndicator.sip @@ -0,0 +1,52 @@ +//Define the SIP wrapper to the QProgressIndicator widget +//Author - Kovid Goyal + +%Module progress_indicator 1 + +%Import QtCore/QtCoremod.sip +%Import QtGui/QtGuimod.sip + +class QProgressIndicator : QWidget { + +%TypeHeaderCode +#include +%End + +public: + + QProgressIndicator(QWidget *parent /TransferThis/ = 0, int size = 64); + + int animationDelay() const; + + bool isAnimated () const; + + bool isDisplayedWhenStopped() const; + + const QColor & color() const; + + virtual QSize sizeHint() const; + + int heightForWidth(int w) const; + + int displaySize() const; + +public slots: + void startAnimation(); + + void stopAnimation(); + + void setAnimationDelay(int delay); + + void setDisplayedWhenStopped(bool state); + + void setColor(const QColor & color); + + void setDisplaySize(int size); + +protected: + + virtual void timerEvent(QTimerEvent * event); + + virtual void paintEvent(QPaintEvent * event); + +}; diff --git a/src/calibre/gui2/progress_indicator/__init__.py b/src/calibre/gui2/progress_indicator/__init__.py new file mode 100644 index 0000000000..5890b26e99 --- /dev/null +++ b/src/calibre/gui2/progress_indicator/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.constants import plugins +pi, pi_error = plugins['progress_indicator'] + +if pi_error: + raise RuntimeError('Failed to load the Progress Indicator plugin: '+\ + pi_error) + +ProgressIndicator = pi.QProgressIndicator diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 16ac1a7ce2..3b421d1afd 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -2,12 +2,13 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' import os, re, collections -from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QWidget, QHBoxLayout, QPixmap, \ +from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ QVBoxLayout, QSizePolicy, QToolButton, QIcon, QScrollArea, QFrame from PyQt4.QtCore import Qt, QSize, SIGNAL, QCoreApplication from calibre import fit_image, preferred_encoding, isosx from calibre.gui2 import qstring_to_unicode, config from calibre.gui2.widgets import IMAGE_EXTENSIONS +from calibre.gui2.progress_indicator import ProgressIndicator from calibre.ebooks import BOOK_EXTENSIONS class BookInfoDisplay(QWidget): @@ -138,14 +139,12 @@ class BookInfoDisplay(QWidget): self.setVisible(True) class MovieButton(QFrame): - def __init__(self, movie, jobs_dialog): + + def __init__(self, jobs_dialog): QFrame.__init__(self) - movie.setCacheMode(QMovie.CacheAll) self.setLayout(QVBoxLayout()) - self.movie_widget = QLabel() - self.movie_widget.setMovie(movie) - self.movie = movie - self.layout().addWidget(self.movie_widget) + self.pi = ProgressIndicator(self) + self.layout().addWidget(self.pi) self.jobs = QLabel(''+_('Jobs:')+' 0') self.jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom) self.layout().addWidget(self.jobs) @@ -156,11 +155,8 @@ class MovieButton(QFrame): self.jobs_dialog = jobs_dialog self.setCursor(Qt.PointingHandCursor) self.setToolTip(_('Click to see list of active jobs.')) - movie.start() - movie.setPaused(True) self.jobs_dialog.jobs_view.restore_column_widths() - def mouseReleaseEvent(self, event): if self.jobs_dialog.isVisible(): self.jobs_dialog.jobs_view.write_settings() @@ -170,6 +166,17 @@ class MovieButton(QFrame): self.jobs_dialog.show() self.jobs_dialog.jobs_view.restore_column_widths() + @property + def is_running(self): + return self.pi.isAnimated() + + def start(self): + self.pi.startAnimation() + + def stop(self): + self.pi.stopAnimation() + + class CoverFlowButton(QToolButton): def __init__(self, parent=None): @@ -211,7 +218,7 @@ class StatusBar(QStatusBar): def __init__(self, jobs_dialog, systray=None): QStatusBar.__init__(self) self.systray = systray - self.movie_button = MovieButton(QMovie(I('jobs-animated.mng')), jobs_dialog) + self.movie_button = MovieButton(jobs_dialog) self.cover_flow_button = CoverFlowButton() self.tag_view_button = TagViewButton() self.addPermanentWidget(self.cover_flow_button) @@ -262,8 +269,7 @@ class StatusBar(QStatusBar): num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) - if self.movie_button.movie.state() == QMovie.Paused: - self.movie_button.movie.setPaused(False) + self.movie_button.start() def job_done(self, nnum): jobs = self.movie_button.jobs @@ -275,49 +281,8 @@ class StatusBar(QStatusBar): self.no_more_jobs() def no_more_jobs(self): - if self.movie_button.movie.state() == QMovie.Running: - self.movie_button.movie.jumpToFrame(0) - self.movie_button.movie.setPaused(True) + if self.movie_button.is_running: + self.movie_button.stop() QCoreApplication.instance().alert(self, 5000) -if __name__ == '__main__': - # Used to create the animated status icon - from PyQt4.Qt import QPainter, QSvgRenderer, QColor - from subprocess import check_call - from calibre.gui2 import is_ok_to_use_qt - is_ok_to_use_qt() - - def create_pixmaps(path, size=16, delta=20): - r = QSvgRenderer(path) - if not r.isValid(): - raise Exception(path + ' not valid svg') - pixmaps = [] - for angle in range(0, 360+delta, delta): - pm = QPixmap(size, size) - pm.fill(QColor(0,0,0,0)) - p = QPainter(pm) - p.translate(size/2., size/2.) - p.rotate(angle) - p.translate(-size/2., -size/2.) - r.render(p) - p.end() - pixmaps.append(pm) - return pixmaps - - def create_mng(path='', size=64, angle=5, delay=5): - pixmaps = create_pixmaps(path, size, angle) - filesl = [] - for i in range(len(pixmaps)): - name = 'a%s.png'%(i,) - filesl.append(name) - pixmaps[i].save(name, 'PNG') - filesc = ' '.join(filesl) - cmd = 'convert -dispose Background -delay '+str(delay)+ ' ' + filesc + ' -loop 0 animated.gif' - try: - check_call(cmd, shell=True) - finally: - for file in filesl: - os.remove(file) - import sys - create_mng(sys.argv[1]) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 233440387f..99ef79bafe 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -5,7 +5,7 @@ import traceback, os, sys, functools, collections, re from functools import partial from threading import Thread -from PyQt4.Qt import QMovie, QApplication, Qt, QIcon, QTimer, QWidget, SIGNAL, \ +from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, \ QDesktopServices, QDoubleSpinBox, QLabel, QTextBrowser, \ QPainter, QBrush, QColor, QStandardItemModel, QPalette, \ QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \ @@ -14,6 +14,7 @@ from PyQt4.Qt import QMovie, QApplication, Qt, QIcon, QTimer, QWidget, SIGNAL, \ from calibre.gui2.viewer.main_ui import Ui_EbookViewer from calibre.gui2.viewer.printing import Printing from calibre.gui2.viewer.bookmarkmanager import BookmarkManager +from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.main_window import MainWindow from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \ info_dialog, error_dialog @@ -62,39 +63,6 @@ class Worker(Thread): self.exception = err self.traceback = traceback.format_exc() -class ProgressIndicator(QWidget): - - def __init__(self, *args): - QWidget.__init__(self, *args) - self.setGeometry(0, 0, 300, 500) - self.movie = QMovie(I('jobs-animated.mng')) - self.ml = QLabel(self) - self.ml.setMovie(self.movie) - self.movie.start() - self.movie.setPaused(True) - self.status = QLabel(self) - self.status.setWordWrap(True) - self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop) - self.setVisible(False) - - def start(self, msg=''): - view = self.parent() - pwidth, pheight = view.size().width(), view.size().height() - self.resize(pwidth, min(pheight, 250)) - self.move(0, (pheight-self.size().height())/2.) - self.ml.resize(self.ml.sizeHint()) - self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0) - self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10) - self.status.move(0, self.ml.size().height()+10) - self.status.setText('

'+msg+'

') - self.setVisible(True) - self.movie.setPaused(False) - - def stop(self): - if self.movie.state() == self.movie.Running: - self.movie.setPaused(True) - self.setVisible(False) - class History(collections.deque): def __init__(self, action_back, action_forward): diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 190ff920ef..7784915b96 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -7,7 +7,7 @@ import re, os, traceback from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \ QListWidgetItem, QTextCharFormat, QApplication, \ QSyntaxHighlighter, QCursor, QColor, QWidget, \ - QPixmap, QMovie, QPalette, QTimer, QDialog, \ + QPixmap, QPalette, QTimer, QDialog, \ QAbstractListModel, QVariant, Qt, SIGNAL, \ QRegExp, QSettings, QSize, QModelIndex, \ QAbstractButton, QPainter, QLineEdit, QComboBox, \ @@ -22,17 +22,14 @@ from calibre.utils.fonts import fontconfig from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.utils.config import prefs +from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator class ProgressIndicator(QWidget): def __init__(self, *args): QWidget.__init__(self, *args) self.setGeometry(0, 0, 300, 350) - self.movie = QMovie(I('jobs-animated.mng')) - self.ml = QLabel(self) - self.ml.setMovie(self.movie) - self.movie.start() - self.movie.setPaused(True) + self.pi = _ProgressIndicator(self) self.status = QLabel(self) self.status.setWordWrap(True) self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop) @@ -43,18 +40,17 @@ class ProgressIndicator(QWidget): pwidth, pheight = view.size().width(), view.size().height() self.resize(pwidth, min(pheight, 250)) self.move(0, (pheight-self.size().height())/2.) - self.ml.resize(self.ml.sizeHint()) - self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0) - self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10) - self.status.move(0, self.ml.size().height()+10) + self.pi.resize(self.pi.sizeHint()) + self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0) + self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10) + self.status.move(0, self.pi.size().height()+10) self.status.setText('

'+msg+'

') self.setVisible(True) - self.movie.setPaused(False) + self.pi.startAnimation() def stop(self): - if self.movie.state() == self.movie.Running: - self.movie.setPaused(True) - self.setVisible(False) + self.pi.stopAnimation() + self.setVisible(False) class FilenamePattern(QWidget, Ui_Form): diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst index b21d92b6aa..72e30eedbb 100644 --- a/src/calibre/manual/conversion.rst +++ b/src/calibre/manual/conversion.rst @@ -433,7 +433,7 @@ settings (if any) or the defaults. .. note:: - You can see the actual settings used during any conversion by clicking the rotating hourglass + You can see the actual settings used during any conversion by clicking the rotating icon in the lower right corner and then double clicking the individual conversion job. This will bring up a conversion log that will contain the actual settings used, near the top. diff --git a/src/calibre/manual/images/jobs.png b/src/calibre/manual/images/jobs.png index edfe5121c8..ab0716af48 100644 Binary files a/src/calibre/manual/images/jobs.png and b/src/calibre/manual/images/jobs.png differ diff --git a/src/calibre/manual/index.rst b/src/calibre/manual/index.rst index 83ff7b0bfe..d736dbd020 100644 --- a/src/calibre/manual/index.rst +++ b/src/calibre/manual/index.rst @@ -9,9 +9,9 @@ So you've just started |app|. What do you do now? Well, before |app| can do anyt .. image:: images/added_books.png -Once you've admired the list of books you just added to your heart's content, you'll probably want to actually read one. In order to do that you'll have to convert the book to a format your reader understands. When first running |app|, the Welcome Wizard starts and it will have setup calibre for your reader device. Conversion is a breeze, just select the book you want to convert, and click the "Convert E-book" button. Ignore all the options for now and just click "OK". The little hourglass in the bottom right corner will start spinning. Once it's finished spinning, your converted book is ready. Click the "View" button to read the book. +Once you've admired the list of books you just added to your heart's content, you'll probably want to actually read one. In order to do that you'll have to convert the book to a format your reader understands. When first running |app|, the Welcome Wizard starts and it will have setup calibre for your reader device. Conversion is a breeze, just select the book you want to convert, and click the "Convert E-book" button. Ignore all the options for now and just click "OK". The little icon in the bottom right corner will start spinning. Once it's finished spinning, your converted book is ready. Click the "View" button to read the book. -Now if you want to read the book on your reader, just connect it to the computer, wait till calibre detects it (10-20secs) and then click the "Send to device" button. Once the hourglass stops spinning again, disconnect your reader and read away! If you didn't convert the book in the previous step, |app| will auto convert it to the format your reader device understands. +Now if you want to read the book on your reader, just connect it to the computer, wait till calibre detects it (10-20secs) and then click the "Send to device" button. Once the icon stops spinning again, disconnect your reader and read away! If you didn't convert the book in the previous step, |app| will auto convert it to the format your reader device understands. To get started with more advanced usage, you should read about the :ref:`Graphical User Interface `. For even more power and versatility, learn the :ref:`Command Line Interface `. diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 75eff33448..57458e344b 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -99,6 +99,7 @@ _extra_lang_codes = { 'en_AU' : _('English (AU)'), 'en_CA' : _('English (CA)'), 'en_IN' : _('English (IND)'), + 'en_TH' : _('English (TH)'), 'nl' : _('Dutch (NL)'), 'nl_BE' : _('Dutch (BE)'), 'und' : _('Unknown')