Switch to using the new custom recipe dialog

This commit is contained in:
Kovid Goyal 2014-11-23 11:55:09 +05:30
parent 63ba84eed1
commit f89b974b14
5 changed files with 8 additions and 971 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -32,10 +32,10 @@ blog into an ebook, we rely on the :term:`RSS` feed of the blog::
http://blog.calibre-ebook.com/feeds/posts/default
I got the RSS URL by looking under "Subscribe to" at the bottom of the blog
page and choosing Posts->Atom. To make calibre download the feeds and convert
page and choosing :guilabel:`Posts->Atom`. To make calibre download the feeds and convert
them into an ebook, you should right click the :guilabel:`Fetch news` button
and then the :guilabel:`Add a custom news source` menu item. A dialog similar
to that shown below should open up.
and then the :guilabel:`Add a custom news source` menu item and then the
:guilabel:`New Recipe` button. A dialog similar to that shown below should open up.
.. image:: images/custom_news.png
:align: center
@ -44,7 +44,9 @@ First enter ``calibre Blog`` into the :guilabel:`Recipe title` field. This will
The next two fields (:guilabel:`Oldest article` and :guilabel:`Max. number of articles`) allow you some control over how many articles should be downloaded from each feed, and they are pretty self explanatory.
To add the feeds to the recipe, enter the feed title and the feed URL and click the :guilabel:`Add feed` button. Once you have added the feed, simply click the :guilabel:`Add/update recipe` button and you're done! Close the dialog.
To add the feeds to the recipe, enter the feed title and the feed URL and click
the :guilabel:`Add feed` button. Once you have added the feed, simply click the
:guilabel:`Save` button and you're done! Close the dialog.
To test your new :term:`recipe`, click the :guilabel:`Fetch news` button and in the :guilabel:`Custom news sources` sub-menu click :guilabel:`calibre Blog`. After a couple of minutes, the newly downloaded ebook of blog posts will appear in the main library view (if you have your reader connected, it will be put onto the reader instead of into the library). Select it and hit the :guilabel:`View` button to read!

View File

@ -489,11 +489,10 @@ class Scheduler(QObject):
self.lock.unlock()
def customize_feeds(self, *args):
from calibre.gui2.dialogs.user_profiles import UserProfiles
d = UserProfiles(self._parent, self.recipe_model)
from calibre.gui2.dialogs.custom_recipes import CustomRecipes
d = CustomRecipes(self.recipe_model, self._parent)
try:
d.exec_()
d.break_cycles()
finally:
d.deleteLater()

View File

@ -1,449 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import time, os
from PyQt5.Qt import (QUrl, QAbstractListModel, Qt, QFont)
from calibre.web.feeds.recipes import compile_recipe, custom_recipes
from calibre.web.feeds.news import AutomaticNewsRecipe
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
from calibre.gui2 import (
error_dialog, question_dialog, open_url, choose_files, ResizableDialog,
open_local_file)
from calibre.gui2.widgets import PythonHighlighter
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.icu import sort_key
class CustomRecipeModel(QAbstractListModel):
def __init__(self, recipe_model):
QAbstractListModel.__init__(self)
self.recipe_model = recipe_model
def title(self, index):
row = index.row()
if row > -1 and row < self.rowCount():
return self.recipe_model.custom_recipe_collection[row].get('title', '')
def script(self, index):
row = index.row()
if row > -1 and row < self.rowCount():
urn = self.recipe_model.custom_recipe_collection[row].get('id')
return self.recipe_model.get_recipe(urn)
def has_title(self, title):
for x in self.recipe_model.custom_recipe_collection:
if x.get('title', False) == title:
return True
return False
def rowCount(self, *args):
try:
return len(self.recipe_model.custom_recipe_collection)
except:
return 0
def data(self, index, role):
if role == Qt.DisplayRole:
ans = self.title(index)
if ans is not None:
return (ans)
return None
def replace_by_title(self, title, script):
urn = None
for x in self.recipe_model.custom_recipe_collection:
if x.get('title', False) == title:
urn = x.get('id')
if urn is not None:
self.beginResetModel()
self.recipe_model.update_custom_recipe(urn, title, script)
self.endResetModel()
def replace_many_by_title(self, scriptmap):
script_urn_map = {}
for title, script in scriptmap.iteritems():
urn = None
for x in self.recipe_model.custom_recipe_collection:
if x.get('title', False) == title:
urn = x.get('id')
if urn is not None:
script_urn_map.update({urn: (title, script)})
if script_urn_map:
self.beginResetModel()
self.recipe_model.update_custom_recipes(script_urn_map)
self.endResetModel()
def add(self, title, script):
self.beginResetModel()
self.recipe_model.add_custom_recipe(title, script)
self.endResetModel()
def add_many(self, scriptmap):
self.beginResetModel()
self.recipe_model.add_custom_recipes(scriptmap)
self.endResetModel()
def remove(self, rows):
urns = []
for r in rows:
try:
urn = self.recipe_model.custom_recipe_collection[r].get('id')
urns.append(urn)
except:
pass
self.beginResetModel()
self.recipe_model.remove_custom_recipes(urns)
self.endResetModel()
class UserProfiles(ResizableDialog, Ui_Dialog):
def __init__(self, parent, recipe_model):
ResizableDialog.__init__(self, parent)
self._model = self.model = CustomRecipeModel(recipe_model)
self.available_profiles.setModel(self._model)
self.available_profiles.currentChanged = self.current_changed
f = QFont()
f.setStyleHint(f.Monospace)
self.source_code.setFont(f)
self.remove_feed_button.clicked[(bool)].connect(self.added_feeds.remove_selected_items)
self.remove_profile_button.clicked[(bool)].connect(self.remove_selected_items)
self.add_feed_button.clicked[(bool)].connect(self.add_feed)
self.load_button.clicked.connect(self.load)
self.opml_button.clicked.connect(self.opml_import)
self.builtin_recipe_button.clicked.connect(self.add_builtin_recipe)
self.share_button.clicked.connect(self.share)
self.show_recipe_files_button.clicked.connect(self.show_recipe_files)
self.down_button.clicked.connect(self.down)
self.up_button.clicked.connect(self.up)
self.add_profile_button.clicked[(bool)].connect(self.add_profile)
self.feed_url.returnPressed[()].connect(self.add_feed)
self.feed_title.returnPressed[()].connect(self.add_feed)
self.toggle_mode_button.clicked[(bool)].connect(self.toggle_mode)
self.clear()
def show_recipe_files(self, *args):
bdir = os.path.dirname(custom_recipes.file_path)
if not os.path.exists(bdir):
return error_dialog(self, _('No recipes'),
_('No custom recipes created.'), show=True)
open_local_file(bdir)
def break_cycles(self):
self.recipe_model = self._model.recipe_model = None
self.available_profiles = None
self.model = self._model = None
def remove_selected_items(self):
indices = self.available_profiles.selectionModel().selectedRows()
self._model.remove([i.row() for i in indices])
self.clear()
def up(self):
row = self.added_feeds.currentRow()
item = self.added_feeds.takeItem(row)
if item is not None:
self.added_feeds.insertItem(max(row-1, 0), item)
self.added_feeds.setCurrentItem(item)
def down(self):
row = self.added_feeds.currentRow()
item = self.added_feeds.takeItem(row)
if item is not None:
self.added_feeds.insertItem(row+1, item)
self.added_feeds.setCurrentItem(item)
def share(self):
index = self.available_profiles.currentIndex()
title, src = self._model.title(index), self._model.script(index)
if not title or not src:
error_dialog(self, _('No recipe selected'), _('No recipe selected')).exec_()
return
pt = PersistentTemporaryFile(suffix='.recipe')
pt.write(src.encode('utf-8'))
pt.close()
body = _('The attached file: %(fname)s is a '
'recipe to download %(title)s.')%dict(
fname=os.path.basename(pt.name), title=title)
subject = _('Recipe for ')+title
url = QUrl('mailto:')
url.addQueryItem('subject', subject)
url.addQueryItem('body', body)
url.addQueryItem('attachment', pt.name)
open_url(url)
def current_changed(self, current, previous):
if not current.isValid():
return
src = self._model.script(current)
if src is None:
return
if 'class BasicUserRecipe' in src:
recipe = compile_recipe(src)
self.populate_options(recipe)
self.stacks.setCurrentIndex(0)
self.toggle_mode_button.setText(_('Switch to Advanced mode'))
self.source_code.setPlainText('')
else:
self.source_code.setPlainText(src)
self.highlighter = PythonHighlighter(self.source_code.document())
self.stacks.setCurrentIndex(1)
self.toggle_mode_button.setText(_('Switch to Basic mode'))
def toggle_mode(self, *args):
if self.stacks.currentIndex() == 1:
self.stacks.setCurrentIndex(0)
self.toggle_mode_button.setText(_('Switch to Advanced mode'))
else:
self.stacks.setCurrentIndex(1)
self.toggle_mode_button.setText(_('Switch to Basic mode'))
if not unicode(self.source_code.toPlainText()).strip():
src = self.options_to_profile()[0].replace('AutomaticNewsRecipe', 'BasicNewsRecipe')
self.source_code.setPlainText(src.replace('BasicUserRecipe', 'AdvancedUserRecipe'))
self.highlighter = PythonHighlighter(self.source_code.document())
def add_feed(self, *args):
title = unicode(self.feed_title.text()).strip()
if not title:
error_dialog(self, _('Feed must have a title'),
_('The feed must have a title')).exec_()
return
url = unicode(self.feed_url.text()).strip()
if not url:
error_dialog(self, _('Feed must have a URL'),
_('The feed %s must have a URL')%title).exec_()
return
try:
self.added_feeds.add_item(title+' - '+url, (title, url))
except ValueError:
error_dialog(self, _('Already exists'),
_('This feed has already been added to the recipe')).exec_()
return
self.feed_title.setText('')
self.feed_url.setText('')
def options_to_profile(self, **kw):
classname = 'BasicUserRecipe'+str(int(time.time()))
title = kw.get('title', self.profile_title.text())
title = unicode(title).strip()
if not title:
title = classname
self.profile_title.setText(title)
oldest_article = kw.get('oldest_article', self.oldest_article.value())
max_articles = kw.get('max_articles', self.max_articles.value())
feeds = kw.get('feeds',
[i.user_data for i in self.added_feeds.items()])
src = '''\
class %(classname)s(%(base_class)s):
title = %(title)s
oldest_article = %(oldest_article)d
max_articles_per_feed = %(max_articles)d
auto_cleanup = True
feeds = %(feeds)s
'''%dict(classname=classname, title=repr(title),
feeds=repr(feeds), oldest_article=oldest_article,
max_articles=max_articles,
base_class='AutomaticNewsRecipe')
return src, title
def populate_source_code(self):
src = self.options_to_profile().replace('BasicUserRecipe', 'AdvancedUserRecipe')
self.source_code.setPlainText(src)
self.highlighter = PythonHighlighter(self.source_code.document())
def add_profile(self, clicked):
if self.stacks.currentIndex() == 0:
src, title = self.options_to_profile()
try:
compile_recipe(src)
except Exception as err:
error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return
profile = src
else:
src = unicode(self.source_code.toPlainText())
try:
title = compile_recipe(src).title
except Exception as err:
error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return
profile = src.replace('BasicUserRecipe', 'AdvancedUserRecipe')
if self._model.has_title(title):
if question_dialog(self, _('Replace recipe?'),
_('A custom recipe named %s already exists. Do you want to '
'replace it?')%title):
self._model.replace_by_title(title, profile)
else:
return
else:
self.model.add(title, profile)
self.clear()
def add_builtin_recipe(self):
from calibre.web.feeds.recipes.collection import \
get_builtin_recipe_collection, get_builtin_recipe_by_id
from PyQt5.Qt import QDialog, QVBoxLayout, QListWidgetItem, \
QListWidget, QDialogButtonBox, QSize
d = QDialog(self)
d.l = QVBoxLayout()
d.setLayout(d.l)
d.list = QListWidget(d)
d.list.doubleClicked.connect(lambda x: d.accept())
d.l.addWidget(d.list)
d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,
Qt.Horizontal, d)
d.bb.accepted.connect(d.accept)
d.bb.rejected.connect(d.reject)
d.l.addWidget(d.bb)
d.setWindowTitle(_('Choose builtin recipe'))
items = []
for r in get_builtin_recipe_collection():
id_ = r.get('id', '')
title = r.get('title', '')
lang = r.get('language', '')
if id_ and title:
items.append((title + ' [%s]'%lang, id_))
items.sort(key=lambda x:sort_key(x[0]))
for title, id_ in items:
item = QListWidgetItem(title)
item.setData(Qt.UserRole, id_)
d.list.addItem(item)
d.resize(QSize(450, 400))
ret = d.exec_()
d.list.doubleClicked.disconnect()
if ret != d.Accepted:
return
items = list(d.list.selectedItems())
if not items:
return
item = items[-1]
id_ = unicode(item.data(Qt.UserRole) or '')
title = unicode(item.data(Qt.DisplayRole) or '').rpartition(' [')[0]
profile = get_builtin_recipe_by_id(id_, download_recipe=True)
if profile is None:
raise Exception('Something weird happened')
if self._model.has_title(title):
if question_dialog(self, _('Replace recipe?'),
_('A custom recipe named %s already exists. Do you want to '
'replace it?')%title):
self._model.replace_by_title(title, profile)
else:
return
else:
self.model.add(title, profile)
self.clear()
def load(self):
files = choose_files(self, 'recipe loader dialog',
_('Choose a recipe file'),
filters=[(_('Recipes'), ['.py', '.recipe'])],
all_files=False, select_only_single_file=True)
if files:
file = files[0]
try:
profile = open(file, 'rb').read().decode('utf-8')
title = compile_recipe(profile).title
except Exception as err:
error_dialog(self, _('Invalid input'),
_('<p>Could not create recipe. Error:<br>%s')%str(err)).exec_()
return
if self._model.has_title(title):
if question_dialog(self, _('Replace recipe?'),
_('A custom recipe named %s already exists. Do you want to '
'replace it?')%title):
self._model.replace_by_title(title, profile)
else:
return
else:
self.model.add(title, profile)
self.clear()
def opml_import(self):
from calibre.gui2.dialogs.opml import ImportOPML
d = ImportOPML(parent=self)
if d.exec_() != d.Accepted:
return
oldest_article, max_articles_per_feed, replace_existing = d.oldest_article, d.articles_per_feed, d.replace_existing
failed_recipes, replace_recipes, add_recipes = {}, {}, {}
for group in d.recipes:
title = base_title = group.title or _('Unknown')
if not replace_existing:
c = 0
while self._model.has_title(title):
c += 1
title = u'%s %d' % (base_title, c)
src, title = self.options_to_profile(**{
'title':title,
'feeds':group.feeds,
'oldest_article':oldest_article,
'max_articles':max_articles_per_feed,
})
try:
compile_recipe(src)
except Exception:
import traceback
failed_recipes[title] = traceback.format_exc()
continue
if replace_existing and self._model.has_title(title):
replace_recipes[title] = src
else:
add_recipes[title] = src
if add_recipes:
self.model.add_many(add_recipes)
if replace_recipes:
self.model.replace_many_by_title(replace_recipes)
if failed_recipes:
det_msg = '\n'.join('%s\n%s\n' % (title, tb) for title, tb in failed_recipes.iteritems())
error_dialog(self, _('Failed to create recipes'), _(
'Failed to create some recipes, click "Show details" for details'), show=True,
det_msg=det_msg)
self.clear()
def populate_options(self, profile):
self.oldest_article.setValue(profile.oldest_article)
self.max_articles.setValue(profile.max_articles_per_feed)
self.profile_title.setText(profile.title)
self.added_feeds.clear()
feeds = [] if profile.feeds is None else profile.feeds
for title, url in feeds:
self.added_feeds.add_item(title+' - '+url, (title, url))
self.feed_title.setText('')
self.feed_url.setText('')
def clear(self):
self.populate_options(AutomaticNewsRecipe)
self.source_code.setText('')
def reject(self):
if question_dialog(self, _('Are you sure?'),
_('You will lose any unsaved changes. To save your'
' changes, click the Add/Update recipe button.'
' Continue?'), show_copy_button=False):
ResizableDialog.reject(self)
if __name__ == '__main__':
from PyQt5.Qt import QApplication
app = QApplication([])
from calibre.web.feeds.recipes.model import RecipeModel
d=UserProfiles(None, RecipeModel())
d.exec_()
del app

View File

@ -1,515 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>738</width>
<height>640</height>
</rect>
</property>
<property name="windowTitle">
<string>Add custom news source</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/user_profile.png</normaloff>:/images/user_profile.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>726</width>
<height>595</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="central_widget" native="true">
<property name="minimumSize">
<size>
<width>580</width>
<height>550</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Available user recipes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListView" name="available_profiles"/>
</item>
<item>
<widget class="QPushButton" name="add_profile_button">
<property name="text">
<string>Add/Update &amp;recipe</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove_profile_button">
<property name="text">
<string>&amp;Remove recipe</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/list_remove.png</normaloff>:/images/list_remove.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="share_button">
<property name="text">
<string>&amp;Share recipe</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="show_recipe_files_button">
<property name="text">
<string>S&amp;how recipe files</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="builtin_recipe_button">
<property name="text">
<string>Customize &amp;builtin recipe</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/news.png</normaloff>:/images/news.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="load_button">
<property name="text">
<string>&amp;Load recipe from file</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="opml_button">
<property name="toolTip">
<string>Import a collection of RSS feeds in OPML format
Many RSS readers can export their subscribed RSS feeds
in OPML format</string>
</property>
<property name="text">
<string>Import &amp;OPML</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/opml.png</normaloff>:/images/mimetypes/opml.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="toggle_mode_button">
<property name="text">
<string>Switch to Advanced mode</string>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stacks">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Create a basic news recipe, by adding RSS feeds to it. &lt;br /&gt;For most feeds, you will have to use the &quot;Advanced mode&quot; to further customize the fetch process.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Recipe &amp;title:</string>
</property>
<property name="buddy">
<cstring>profile_title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="EnLineEdit" name="profile_title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Oldest article:</string>
</property>
<property name="buddy">
<cstring>oldest_article</cstring>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="oldest_article">
<property name="toolTip">
<string>The oldest article to download</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>36500</number>
</property>
<property name="value">
<number>7</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Max. number of articles per feed:</string>
</property>
<property name="buddy">
<cstring>max_articles</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="max_articles">
<property name="toolTip">
<string>Maximum number of articles to download per feed.</string>
</property>
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Feeds in recipe</string>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="BasicList" name="added_feeds">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout">
<item>
<widget class="QToolButton" name="up_button">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_feed_button">
<property name="toolTip">
<string>Remove feed from recipe</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/list_remove.png</normaloff>:/images/list_remove.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="down_button">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Add feed to recipe</string>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Feed title:</string>
</property>
<property name="buddy">
<cstring>feed_title</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="EnLineEdit" name="feed_title"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Feed &amp;URL:</string>
</property>
<property name="buddy">
<cstring>feed_url</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="feed_url"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="add_feed_button">
<property name="toolTip">
<string>Add feed to recipe</string>
</property>
<property name="text">
<string>&amp;Add feed</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>For help with writing advanced news recipes, please visit &lt;a href=&quot;http://manual.calibre-ebook.com/news.html&quot;&gt;User Recipes&lt;/a&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Recipe source code (python)</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QTextEdit" name="source_code">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="lineWrapMode">
<enum>QTextEdit::NoWrap</enum>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BasicList</class>
<extends>QListWidget</extends>
<header>calibre/gui2/widgets.h</header>
</customwidget>
<customwidget>
<class>EnLineEdit</class>
<extends>QLineEdit</extends>
<header>calibre/gui2/widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>446</x>
<y>649</y>
</hint>
<hint type="destinationlabel">
<x>0</x>
<y>632</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>175</x>
<y>643</y>
</hint>
<hint type="destinationlabel">
<x>176</x>
<y>636</y>
</hint>
</hints>
</connection>
</connections>
</ui>