Sync to trunk.

This commit is contained in:
John Schember 2011-01-28 20:05:40 -05:00
commit b435e375e6
93 changed files with 30746 additions and 18437 deletions

View File

@ -4,12 +4,137 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
#- version: ?.?.?
# date: 2011-??-??
#
# new features:
# - title:
#
# bug fixes:
# - title:
#
# improved recipes:
# -
#
# new recipes:
# - title:
- version: 0.7.43
date: 2011-01-28
new features:
- title: "Ask for confirmation when stopping running jobs"
tickets: [3101]
- title: "Combine the database integrity check and library check into a single menu item. Also nicer implementation of the db integrity check."
- title: "BiBTeX Catalog: Add option to include file paths in the catalog."
tickets: [8589]
- title: "Create 'generic' output profiles and generic devices in the welcome wizard"
- title: "Bulk metadata edit: Custom column widgets all have an apply checkbox next to them."
- title: "Only use LibraryThing to download metadata if the user provides a library thing username and password. Since LT doesn't like web scraping"
- title: "Allow renaming of user categories in the manage categories dialog. Also allow searching for books in a category from the Tag Browser by right clicking ona a category"
- title: "Folder device plugin: Add option to disable the use of sub folders"
- title: "Allow saving/loading of search and replace expressions in the bulk metadata edit dialog."
- title: "Remeber previously used regular expression in the add books preferences dialog"
- title: "Search and replace wizard: Cache the previously used input document."
- title: "Pressing Esc clears the current search in the main book list"
- title: "Preselct right formats when using send specific format to device"
tickets: [7834]
- title: "Regex wizard gets find next and previous match buttons"
tickets: [4486]
bug fixes:
- title: "Do not allow customization of user interface plugins until calibre is restarted"
tickets: [8621]
- title: "EPUB Output: When using preserve cover aspect ratio, use the actual image sizes in the SVG template as otherwise ADE doesn't fully preserve the aspect ratio"
- title: "Fix completion on a word with a trailing space causing the first letter to be duplicated in the edit metadata dialog"
- title: "PML Input: PML x and Xn tags don't indent properly in TOC. Also handle invalid T markup and retain soft scene breaks"
tickets: [6194, 8565]
- title: "TXT Input: Retain whitespace at the beginning of lines. Don't preserve spaces in heuristic processing. Detect and retain soft scene breaks."
- title: "Fix Adding empty book - cover browser doesn't update"
tickets: [8557]
- title: "When generating author sort string ignore trailing Inc."
tickets: [8539]
- title: "When converting HTML/ZIP files do not leave temporary files that are only deleted on application shutdown."
tickets: [8597]
- title: "Don't crash if the prefs stored in the db are corrupted"
- title: "Catalog generation: Do not use inline-block CSS as apparently Adobe Digital Editions cannot handle it."
tickets: [8566]
- title: "Fix extra spaces being inserted into TOC title when reading TOC from OPF guide element."
tickets: [8569]
- title: "Remember window size for bulk metadata edit and catalog generation dialogs"
tickets: [8525]
- title: "Heuristics, italicize common cases: Enhance pattern matching to match punctuation after pattern."
- title: "Fix regression in converting HTML files that have ASCII-encoded non unicode characters inside their <style> tags. Apparently Word generates these."
tickets: [8494]
improved recipes:
- Calgary Herald
- The Economist
- New Yorker
- Heise
- HNA
- ZDNet
- NRC Handelsblad
new recipes:
- title: "SPIN Magazine"
author: Quistopher
- title: "Caps n Babes"
author: skyhawker
- title: "Leduc"
author: Brian Hahn
- title: "David Bravo's Blog, La Nueva Espana, 20 Minutos and La Tribuna de Talavera"
author: Luis Hernandez
- title: "Sinfest"
author: nadid
- title: "Various Czech news sources"
author: FunThomas
- title: "tportal.h"
author: Darko Miletic
- title: "Everett Herald"
author: "77jag5"
- title: "Roger Ebert"
author: Shane Erstad
- version: 0.7.42
date: 2011-01-21
new features:
- title: "0.7.42 is a re-release of 0.7.41, because conversion to MOBI was broken in 0.7.41"
- title: "Conversions: Replace the remove header/footer options with a more geenric search replace option, that allows you to not only remove but also replace text"
- title: "Conversion: The preprocess html option has now become a new 'Heuristic Processing' option which allows you to control exactly which heuristics are used"

View File

@ -44,6 +44,7 @@ class CanWestPaper(BasicNewsRecipe):
language = 'en_CA'
__author__ = 'Nick Redding'
encoding = 'latin1'
no_stylesheets = True
timefmt = ' [%b %d]'
extra_css = '''
@ -97,7 +98,9 @@ class CanWestPaper(BasicNewsRecipe):
atag = h1tag.find('a',href=True)
if not atag:
continue
url = self.url_prefix+'/news/todays-paper/'+atag['href']
url = atag['href']
if not url.startswith('http:'):
url = self.url_prefix+'/news/todays-paper/'+atag['href']
#self.log("Section %s" % key)
#self.log("url %s" % url)
title = self.tag_to_string(atag,False)

View File

@ -0,0 +1,11 @@
from calibre.web.feeds.news import BasicNewsRecipe
class CapesnBabesRecipe(BasicNewsRecipe):
title = u'Capes n Babes'
language = 'en'
description = 'The Capes n Babes comic Blog'
__author__ = 'skyhawker'
oldest_article = 31
max_articles_per_feed = 100
use_embedded_content = True
feeds = [(u'Capes & Babes', u'feed://www.capesnbabes.com/feed/')]

View File

@ -26,7 +26,7 @@ class BBC(BasicNewsRecipe):
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
feeds = [
('Interviews', 'http://www.avclub.com/feed/interview/'),
('TV', 'http://www.avclub.com/feed/tv/'),
('AV Club Daily', 'http://www.avclub.com/feed/daily'),
('Film', 'http://www.avclub.com/feed/film/'),
('Music', 'http://www.avclub.com/feed/music/'),

View File

@ -0,0 +1,15 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1296179411(BasicNewsRecipe):
title = u'SPIN Magzine'
__author__ = 'Quistopher'
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
feeds = [
(u'Daily Noise Blog | SPIN.com', u'http://www.spin.com/blog/feed'),
(u'It Happened Last Night | SPIN.com', u'http://www.spin.com/it-happened-last-night/feed'),
(u'Album Reviews | SPIN.com', u'http://www.spin.com/album-reviews/feed')
]

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.42'
__version__ = '0.7.43'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -19,7 +19,8 @@ class ANDROID(USBMS):
VENDOR_ID = {
# HTC
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100, 0x0227], 0x0ff9
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100,
0x0227, 0x0226], 0x0ff9
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226],
0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]},

View File

@ -141,8 +141,8 @@ class CoverManager(object):
if width is None or height is None:
self.log.warning('Failed to read cover dimensions')
width, height = 600, 800
if self.preserve_aspect_ratio:
width, height = 600, 800
#if self.preserve_aspect_ratio:
# width, height = 600, 800
self.svg_template = self.svg_template.replace('__viewbox__',
'0 0 %d %d'%(width, height))
self.svg_template = self.svg_template.replace('__width__',

View File

@ -47,7 +47,7 @@ class PDFOutput(OutputFormatPlugin):
OptionRecommendation(name='preserve_cover_aspect_ratio',
recommended_value=False,
help=_('Preserve the aspect ratio of the cover, instead'
' of stretching it to fill the ull first page of the'
' of stretching it to fill the full first page of the'
' generated pdf.')
),
])

View File

@ -197,14 +197,10 @@ def error_dialog(parent, title, msg, det_msg='', show=False,
return d.exec_()
return d
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False,
buttons=None, yes_button=None):
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False):
from calibre.gui2.dialogs.message_box import MessageBox
d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent,
show_copy_button=show_copy_button)
if buttons is not None:
d.bb.setStandardButtons(buttons)
return d.exec_() == d.Accepted
def info_dialog(parent, title, msg, det_msg='', show=False,

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil
from functools import partial
from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog
from PyQt4.Qt import QMenu, Qt, QInputDialog
from calibre import isbytestring
from calibre.constants import filesystem_encoding
@ -16,7 +16,7 @@ from calibre.utils.config import prefs
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
question_dialog, info_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
class LibraryUsageStats(object): # {{{
@ -76,76 +76,6 @@ class LibraryUsageStats(object): # {{{
self.write_stats()
# }}}
# Check Integrity {{{
class VacThread(QThread):
check_done = pyqtSignal(object, object)
callback = pyqtSignal(object, object)
def __init__(self, parent, db):
QThread.__init__(self, parent)
self.db = db
self._parent = parent
def run(self):
err = bad = None
try:
bad = self.db.check_integrity(self.callbackf)
except:
import traceback
err = traceback.format_exc()
self.check_done.emit(bad, err)
def callbackf(self, progress, msg):
self.callback.emit(progress, msg)
class CheckIntegrity(QProgressDialog):
def __init__(self, db, parent=None):
QProgressDialog.__init__(self, parent)
self.db = db
self.setCancelButton(None)
self.setMinimum(0)
self.setMaximum(100)
self.setWindowTitle(_('Checking database integrity'))
self.setAutoReset(False)
self.setValue(0)
self.vthread = VacThread(self, db)
self.vthread.check_done.connect(self.check_done,
type=Qt.QueuedConnection)
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
self.vthread.start()
def callback(self, progress, msg):
self.setLabelText(msg)
self.setValue(int(100*progress))
def check_done(self, bad, err):
if err:
error_dialog(self, _('Error'),
_('Failed to check database integrity'),
det_msg=err, show=True)
elif bad:
titles = [self.db.title(x, index_is_id=True) for x in bad]
det_msg = '\n'.join(titles)
warning_dialog(self, _('Some inconsistencies found'),
_('The following books had formats or covers listed in the '
'database that are not actually available. '
'The entries for the formats/covers have been removed. '
'You should check them manually. This can '
'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True)
else:
info_dialog(self, _('No errors found'),
_('The integrity check completed with no uncorrectable errors found.'),
show=True)
self.reset()
# }}}
class ChooseLibraryAction(InterfaceAction):
name = 'Choose Library'
@ -209,14 +139,6 @@ class ChooseLibraryAction(InterfaceAction):
None, None), attr='action_check_library')
ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Check database integrity'), 'lt.png',
None, None), attr='action_check_database')
ac.triggered.connect(self.check_database, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Recover database'), 'lt.png',
None, None), attr='action_restore_database')
ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
self.choose_menu.addMenu(self.maintenance_menu)
def pick_random(self, *args):
@ -346,28 +268,39 @@ class ChooseLibraryAction(InterfaceAction):
'rate of approximately 1 book every three seconds.'), show=True)
def check_library(self):
db = self.gui.library_view.model().db
d = CheckLibraryDialog(self.gui.parent(), db)
d.exec_()
def check_database(self, *args):
self.gui.library_view.save_state()
m = self.gui.library_view.model()
m.stop_metadata_backup()
try:
d = CheckIntegrity(m.db, self.gui)
d.exec_()
finally:
m.start_metadata_backup()
db = m.db
db.prefs.disable_setting = True
def restore_database(self):
info_dialog(self.gui, _('Recover database'), '<p>'+
_(
'This command rebuilds your calibre database from the information '
'stored by calibre in the OPF files.<p>'
'This function is not currently available in the GUI. You can '
'recover your database using the \'calibredb restore_database\' '
'command line function.'
), show=True)
d = DBCheck(self.gui, db)
d.start()
try:
d.conn.close()
except:
pass
d.break_cycles()
self.gui.library_moved(db.library_path, call_close=not
d.closed_orig_conn)
if d.rejected:
return
if d.error is None:
if not question_dialog(self.gui, _('Success'),
_('Found no errors in your calibre library database.'
' Do you want calibre to check if the files in your '
' library match the information in the database?')):
return
else:
return error_dialog(self.gui, _('Failed'),
_('Database integrity check failed, click Show details'
' for details.'), show=True, det_msg=d.error[1])
d = CheckLibraryDialog(self.gui, m.db)
if not d.do_exec():
info_dialog(self.gui, _('No problems found'),
_('The files in your library match the information '
'in the database.'), show=True)
def switch_requested(self, location):
if not self.change_library_allowed():

View File

@ -19,7 +19,9 @@ class PluginWidget(QWidget, Ui_Form):
('bib_entry', 0), #mixed
('bibfile_enc', 0), #utf-8
('bibfile_enctag', 0), #strict
('impcit', True) ]
('impcit', True),
('addfiles', False),
]
sync_enabled = False
formats = set(['bib'])
@ -49,7 +51,7 @@ class PluginWidget(QWidget, Ui_Form):
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
getattr(self, opt[0]).setCurrentIndex(opt_value)
elif opt[0] == 'impcit' :
elif opt[0] in ['impcit', 'addfiles'] :
getattr(self, opt[0]).setChecked(opt_value)
else:
getattr(self, opt[0]).setText(opt_value)
@ -76,7 +78,7 @@ class PluginWidget(QWidget, Ui_Form):
for opt in self.OPTION_FIELDS:
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
opt_value = getattr(self,opt[0]).currentIndex()
elif opt[0] == 'impcit' :
elif opt[0] in ['impcit', 'addfiles'] :
opt_value = getattr(self, opt[0]).isChecked()
else :
opt_value = unicode(getattr(self, opt[0]).text())

View File

@ -47,7 +47,7 @@
</item>
</widget>
</item>
<item row="1" column="1" rowspan="12">
<item row="1" column="1" rowspan="11">
<widget class="QListWidget" name="db_fields">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
@ -141,6 +141,13 @@
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="addfiles">
<property name="text">
<string>Add files path with formats?</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Expression to form the BibTeX citation tag:</string>

View File

@ -7,7 +7,7 @@ import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
Qt, pyqtSignal, QDialog, QMessageBox
Qt, pyqtSignal, QDialog
from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins
@ -609,10 +609,8 @@ class DeviceMixin(object): # {{{
autos = u'\n'.join(map(unicode, map(force_unicode, autos)))
return self.ask_a_yes_no_question(
_('No suitable formats'), msg,
buttons=QMessageBox.Yes|QMessageBox.Cancel,
ans_when_user_unavailable=True,
det_msg=autos,
show_copy_button=False
det_msg=autos
)
def set_default_thumbnail(self, height):

View File

@ -3,16 +3,132 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
import os
import os, shutil
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
QLineEdit, Qt
QLineEdit, Qt, QProgressBar, QSize, QTimer
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.library.check_library import CheckLibrary, CHECKS
from calibre.library.database2 import delete_file, delete_tree
from calibre import prints
from calibre import prints, as_unicode
from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.sqlite import DBThread, OperationalError
class DBCheck(QDialog):
def __init__(self, parent, db):
QDialog.__init__(self, parent)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.l1 = QLabel(_('Checking database integrity')+'...')
self.setWindowTitle(_('Checking database integrity'))
self.l.addWidget(self.l1)
self.pb = QProgressBar(self)
self.l.addWidget(self.pb)
self.pb.setMaximum(0)
self.pb.setMinimum(0)
self.msg = QLabel('')
self.l.addWidget(self.msg)
self.msg.setWordWrap(True)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.l.addWidget(self.bb)
self.bb.rejected.connect(self.reject)
self.resize(self.sizeHint() + QSize(100, 50))
self.error = None
self.db = db
self.closed_orig_conn = False
def start(self):
self.user_version = self.db.user_version
self.rejected = False
self.db.clean()
self.db.conn.close()
self.closed_orig_conn = True
t = DBThread(self.db.dbpath, False)
t.connect()
self.conn = t.conn
self.dump = self.conn.iterdump()
self.statements = []
self.count = 0
self.msg.setText(_('Dumping database to SQL'))
# Give the backup thread time to stop
QTimer.singleShot(2000, self.do_one_dump)
self.exec_()
def do_one_dump(self):
if self.rejected:
return
try:
try:
self.statements.append(self.dump.next())
self.count += 1
except StopIteration:
self.start_load()
return
QTimer.singleShot(0, self.do_one_dump)
except Exception, e:
import traceback
self.error = (as_unicode(e), traceback.format_exc())
self.reject()
def start_load(self):
self.conn.close()
self.pb.setMaximum(self.count)
self.pb.setValue(0)
self.msg.setText(_('Loading database from SQL'))
self.db.conn.close()
self.ndbpath = PersistentTemporaryFile('.db')
self.ndbpath.close()
self.ndbpath = self.ndbpath.name
t = DBThread(self.ndbpath, False)
t.connect()
self.conn = t.conn
self.conn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
self.conn.commit()
QTimer.singleShot(0, self.do_one_load)
def do_one_load(self):
if self.rejected:
return
if self.count > 0:
try:
try:
self.conn.execute(self.statements.pop(0))
except OperationalError:
if self.count > 1:
# The last statement in the dump could be an extra
# commit, so ignore it.
raise
self.pb.setValue(self.pb.value() + 1)
self.count -= 1
QTimer.singleShot(0, self.do_one_load)
except Exception, e:
import traceback
self.error = (as_unicode(e), traceback.format_exc())
self.reject()
else:
self.replace_db()
def replace_db(self):
self.conn.commit()
self.conn.execute('pragma user_version=%d'%int(self.user_version))
self.conn.commit()
self.conn.close()
shutil.copyfile(self.ndbpath, self.db.dbpath)
self.db = None
self.accept()
def break_cycles(self):
self.statements = self.unpickler = self.db = self.conn = None
def reject(self):
self.rejected = True
QDialog.reject(self)
class Item(QTreeWidgetItem):
pass
@ -23,7 +139,7 @@ class CheckLibraryDialog(QDialog):
QDialog.__init__(self, parent)
self.db = db
self.setWindowTitle(_('Check Library'))
self.setWindowTitle(_('Check Library -- Problems Found'))
self._layout = QVBoxLayout(self)
self.setLayout(self._layout)
@ -32,7 +148,7 @@ class CheckLibraryDialog(QDialog):
self.log.itemChanged.connect(self.item_changed)
self._layout.addWidget(self.log)
self.check_button = QPushButton(_('&Run the check'))
self.check_button = QPushButton(_('&Run the check again'))
self.check_button.setDefault(False)
self.check_button.clicked.connect(self.run_the_check)
self.copy_button = QPushButton(_('Copy &to clipboard'))
@ -80,8 +196,17 @@ class CheckLibraryDialog(QDialog):
self.resize(750, 500)
self.bbox.setEnabled(True)
def do_exec(self):
self.run_the_check()
probs = 0
for c in self.problem_count:
probs += self.problem_count[c]
if probs == 0:
return False
self.exec_()
return True
def accept(self):
self.db.prefs['check_library_ignore_extensions'] = \
unicode(self.ext_ignores.text())
@ -103,7 +228,10 @@ class CheckLibraryDialog(QDialog):
attr, h, checkable, fixable = check
list = getattr(checker, attr, None)
if list is None:
self.problem_count[attr] = 0
return
else:
self.problem_count[attr] = len(list)
tl = Item()
tl.setText(0, h)
@ -134,6 +262,7 @@ class CheckLibraryDialog(QDialog):
t.setHeaderLabels([_('Name'), _('Path from library')])
self.all_items = []
self.top_level_items = {}
self.problem_count = {}
for check in CHECKS:
builder(t, checker, check)

View File

@ -14,7 +14,7 @@
<string>Active Jobs</string>
</property>
<property name="windowIcon">
<iconset>
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/jobs.png</normaloff>:/images/jobs.png</iconset>
</property>
<layout class="QVBoxLayout">
@ -44,16 +44,16 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="details_button">
<widget class="QPushButton" name="kill_button">
<property name="text">
<string>Show job &amp;details</string>
<string>&amp;Stop selected job</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="kill_button">
<widget class="QPushButton" name="details_button">
<property name="text">
<string>&amp;Stop selected job</string>
<string>Show job &amp;details</string>
</property>
</widget>
</item>

View File

@ -39,6 +39,12 @@ class MessageBox(QDialog, Ui_Dialog):
self.det_msg.setPlainText(det_msg)
self.det_msg.setVisible(False)
if show_copy_button:
self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
self.bb.ActionRole)
self.ctc_button.clicked.connect(self.copy_to_clipboard)
if det_msg:
self.show_det_msg = _('Show &details')
self.hide_det_msg = _('Hide &details')
@ -47,11 +53,6 @@ class MessageBox(QDialog, Ui_Dialog):
self.det_msg_toggle.setToolTip(
_('Show detailed information about this error'))
if show_copy_button:
self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
self.bb.ActionRole)
self.ctc_button.clicked.connect(self.copy_to_clipboard)
self.copy_action = QAction(self)
self.addAction(self.copy_action)
@ -91,7 +92,10 @@ class MessageBox(QDialog, Ui_Dialog):
def showEvent(self, ev):
ret = QDialog.showEvent(self, ev)
if self.is_question:
self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason)
try:
self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason)
except:
pass# Buttons were changed
else:
self.bb.button(self.bb.Ok).setFocus(Qt.OtherFocusReason)
return ret

View File

@ -7,7 +7,7 @@ import re, os
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
pyqtSignal, QDialogButtonBox, QInputDialog, QLineEdit, \
QMessageBox, QDate
QDate
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
@ -15,7 +15,8 @@ from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, gprefs
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE, \
gprefs, question_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.config import dynamic, JSONConfig
from calibre.utils.titlecase import titlecase
@ -888,12 +889,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.query_field.currentIndex() == 0:
return
ret = QMessageBox.question(self, _("Delete saved search/replace"),
if not question_dialog(self, _("Delete saved search/replace"),
_("The selected saved search/replace will be deleted. "
"Are you sure?"),
QMessageBox.Ok, QMessageBox.Cancel)
if ret == QMessageBox.Cancel:
"Are you sure?")):
return
item_id = self.query_field.currentIndex()
@ -917,11 +915,9 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
new = True
name = unicode(name)
if name in self.queries.keys():
ret = QMessageBox.question(self, _("Save search/replace"),
if not question_dialog(self, _("Save search/replace"),
_("That saved search/replace already exists and will be overwritten. "
"Are you sure?"),
QMessageBox.Ok, QMessageBox.Cancel)
if ret == QMessageBox.Cancel:
"Are you sure?")):
return
new = False

View File

@ -11,7 +11,7 @@ from functools import partial
from threading import Thread
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox, QIcon, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \
QPushButton
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
@ -208,6 +208,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
from calibre.gui2 import config
title = unicode(self.title.text()).strip()
author = unicode(self.authors.text()).strip()
if author.endswith('&'):
author = author[:-1].strip()
if not title or not author:
return error_dialog(self, _('Specify title and author'),
_('You must specify a title and author before generating '
@ -768,9 +770,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if question_dialog(self, _('Tags changed'),
_('You have changed the tags. In order to use the tags'
' editor, you must either discard or apply these '
'changes'), show_copy_button=False,
buttons=QMessageBox.Apply|QMessageBox.Discard,
yes_button=QMessageBox.Apply):
'changes. Apply changes?'), show_copy_button=False):
self.apply_tags(commit=True, notify=True)
self.original_tags = unicode(self.tags.text())
else:

View File

@ -259,14 +259,14 @@ class Scheduler(QObject):
if self.oldest > 0:
delta = timedelta(days=self.oldest)
try:
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
ids = list(self.recipe_model.db.tags_older_than(_('News'),
delta))
except:
# Should never happen
ids = []
import traceback
traceback.print_exc()
if ids:
ids = list(ids)
if ids:
self.delete_old_news.emit(ids)
QTimer.singleShot(60 * 60 * 1000, self.oldest_check)

View File

@ -425,7 +425,6 @@ class JobsDialog(QDialog, Ui_JobsDialog):
for index in self.jobs_view.selectedIndexes():
row = index.row()
self.model.kill_job(row, self)
return
def kill_all_jobs(self, *args):
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop all non-device jobs?')):

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, time, socket, traceback
from functools import partial
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
from PyQt4.Qt import QCoreApplication, QIcon, QObject, QTimer, \
QThread, pyqtSignal, Qt, QProgressDialog, QString, QPixmap, \
QSplashScreen, QApplication
@ -319,9 +319,6 @@ def run_gui(opts, args, actions, listener, app, gui_debug=None):
def cant_start(msg=_('If you are sure it is not running')+', ',
what=None):
d = QMessageBox(QMessageBox.Critical, _('Cannot Start ')+__appname__,
'<p>'+(_('%s is already running.')%__appname__)+'</p>',
QMessageBox.Ok)
base = '<p>%s</p><p>%s %s'
where = __appname__ + ' '+_('may be running in the system tray, in the')+' '
if isosx:
@ -334,8 +331,10 @@ def cant_start(msg=_('If you are sure it is not running')+', ',
else:
what = _('try deleting the file')+': '+ADDRESS
d.setInformativeText(base%(where, msg, what))
d.exec_()
info = base%(where, msg, what)
error_dialog(None, _('Cannot Start ')+__appname__,
'<p>'+(_('%s is already running.')%__appname__)+'</p>'+info, show=True)
raise SystemExit(1)
def communicate(args):

View File

@ -87,7 +87,7 @@ class MainWindow(QMainWindow):
fe = sio.getvalue()
prints(fe, file=sys.stderr)
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
error_dialog(self, _('Unhandled exception'), msg, det_msg=fe,
show=True)
except BaseException:
pass

View File

@ -10,7 +10,7 @@ import textwrap, re, os
from PyQt4.Qt import Qt, QDateEdit, QDate, \
QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
QPushButton, QSpinBox, QMessageBox, QLineEdit
QPushButton, QSpinBox, QLineEdit
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
EnComboBox, FormatList, ImageView, CompleteLineEdit
@ -848,9 +848,7 @@ class TagsEdit(CompleteLineEdit): # {{{
if question_dialog(self, _('Tags changed'),
_('You have changed the tags. In order to use the tags'
' editor, you must either discard or apply these '
'changes'), show_copy_button=False,
buttons=QMessageBox.Apply|QMessageBox.Discard,
yes_button=QMessageBox.Apply):
'changes. Apply changes?'), show_copy_button=False):
self.commit(db, id_)
db.commit()
self.original_val = self.current_val

View File

@ -188,6 +188,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.plugin_view.PositionAtCenter)
self.plugin_view.scrollTo(idx,
self.plugin_view.PositionAtCenter)
self.plugin_view.selectionModel().select(idx,
self.plugin_view.selectionModel().ClearAndSelect)
self.plugin_view.setCurrentIndex(idx)
else:
error_dialog(self, _('No valid plugin path'),
_('%s is not a valid plugin path')%path).exec_()
@ -220,10 +223,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
_('Plugin: %s does not need customization')%plugin.name).exec_()
return
self.changed_signal.emit()
from calibre.customize import InterfaceActionBase
if isinstance(plugin, InterfaceActionBase) and not getattr(plugin,
'actual_iaction_plugin_loaded', False):
return error_dialog(self, _('Must restart'),
_('You must restart calibre before you can'
' configure the <b>%s</b> plugin')%plugin.name, show=True)
if plugin.do_user_config():
self._plugin_model.refresh_plugin(plugin)
elif op == 'remove':
msg = _('Plugin {0} successfully removed').format(plugin.name)
msg = _('Plugin <b>{0}</b> successfully removed').format(plugin.name)
if remove_plugin(plugin):
self._plugin_model.populate()
self._plugin_model.reset()

View File

@ -12,11 +12,9 @@ __docformat__ = 'restructuredtext en'
import collections, os, sys, textwrap, time
from Queue import Queue, Empty
from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, \
QMessageBox, QHelpEvent, QAction
from PyQt4.Qt import Qt, SIGNAL, QTimer, QHelpEvent, QAction, \
QMenu, QIcon, pyqtSignal, \
QDialog, QSystemTrayIcon, QApplication, QKeySequence
from calibre import prints
from calibre.constants import __appname__, isosx
@ -101,28 +99,40 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.opts = opts
self.device_connected = None
self.gui_debug = gui_debug
acmap = OrderedDict()
self.iactions = OrderedDict()
for action in interface_actions():
if opts.ignore_plugins and action.plugin_path is not None:
continue
try:
ac = action.load_actual_plugin(self)
ac = self.init_iaction(action)
except:
# Ignore errors in loading user supplied plugins
import traceback
traceback.print_exc()
if ac.plugin_path is None:
if action.plugin_path is None:
raise
continue
ac.plugin_path = action.plugin_path
ac.interface_action_base_plugin = action
if ac.name in acmap:
if ac.priority >= acmap[ac.name].priority:
acmap[ac.name] = ac
else:
acmap[ac.name] = ac
self.iactions = acmap
self.add_iaction(ac)
def init_iaction(self, action):
ac = action.load_actual_plugin(self)
ac.plugin_path = action.plugin_path
ac.interface_action_base_plugin = action
action.actual_iaction_plugin_loaded = True
return ac
def add_iaction(self, ac):
acmap = self.iactions
if ac.name in acmap:
if ac.priority >= acmap[ac.name].priority:
acmap[ac.name] = ac
else:
acmap[ac.name] = ac
def initialize(self, library_path, db, listener, actions, show_gui=True):
opts = self.opts
@ -345,11 +355,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def is_minimized_to_tray(self):
return getattr(self, '__systray_minimized', False)
def ask_a_yes_no_question(self, title, msg, **kwargs):
awu = kwargs.pop('ans_when_user_unavailable', True)
def ask_a_yes_no_question(self, title, msg, det_msg='',
show_copy_button=False, ans_when_user_unavailable=True):
if self.is_minimized_to_tray:
return awu
return question_dialog(self, title, msg, **kwargs)
return ans_when_user_unavailable
return question_dialog(self, title, msg, det_msg=det_msg,
show_copy_button=show_copy_button)
def hide_windows(self):
for window in QApplication.topLevelWidgets():
@ -408,7 +419,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def library_moved(self, newloc, copy_structure=False):
def library_moved(self, newloc, copy_structure=False, call_close=True):
if newloc is None: return
default_prefs = None
try:
@ -441,7 +452,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.apply_named_search_restriction(db.prefs['gui_restriction'])
if olddb is not None:
try:
olddb.conn.close()
if call_close:
olddb.conn.close()
except:
import traceback
traceback.print_exc()
@ -588,11 +600,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
Quitting may cause corruption on the device.<br>
Are you sure you want to quit?''')+'</p>'
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
QMessageBox.Yes|QMessageBox.No, self)
d.setIconPixmap(QPixmap(I('dialog_warning.png')))
d.setDefaultButton(QMessageBox.No)
if d.exec_() != QMessageBox.Yes:
if not question_dialog(self, _('Active jobs'), msg):
return False
return True

View File

@ -24,10 +24,9 @@ from calibre.utils.logging import default_log as log
from calibre.utils.zipfile import ZipFile, ZipInfo
from calibre.utils.magick.draw import thumbnail
FIELDS = ['all', 'author_sort', 'authors', 'comments',
'cover', 'formats', 'id', 'isbn', 'ondevice', 'pubdate', 'publisher', 'rating',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
'rating', 'series_index', 'series', 'size', 'tags', 'timestamp', 'uuid']
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
@ -54,8 +53,10 @@ class CSV_XML(CatalogPlugin): # {{{
'database. Should be a comma-separated list of fields.\n'
'Available fields: %s,\n'
'plus user-created custom fields.\n'
'Example: %s=title,authors,tags\n'
"Default: '%%default'\n"
"Applies to: CSV, XML output formats")%', '.join(FIELDS)),
"Applies to: CSV, XML output formats")%(', '.join(FIELDS),
'--fields')),
Option('--sort-by',
default = 'id',
@ -231,8 +232,10 @@ class BIBTEX(CatalogPlugin): # {{{
help = _('The fields to output when cataloging books in the '
'database. Should be a comma-separated list of fields.\n'
'Available fields: %s.\n'
'Example: %s=title,authors,tags\n'
"Default: '%%default'\n"
"Applies to: BIBTEX output format")%', '.join(FIELDS)),
"Applies to: BIBTEX output format")%(', '.join(FIELDS),
'--fields')),
Option('--sort-by',
default = 'id',
@ -252,6 +255,15 @@ class BIBTEX(CatalogPlugin): # {{{
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--add-files-path',
default = 'True',
dest = 'addfiles',
action = None,
help = _('Create a file entry if formats is selected for BibTeX entries.\n'
'Boolean value: True, False\n'
"Default: '%default'\n"
"Applies to: BIBTEX output format")),
Option('--citation-template',
default = '{authors}{id}',
dest = 'bib_cit',
@ -298,7 +310,7 @@ class BIBTEX(CatalogPlugin): # {{{
from calibre.utils.bibtex import BibTeX
def create_bibtex_entry(entry, fields, mode, template_citation,
bibtexdict, citation_bibtex = True):
bibtexdict, citation_bibtex=True, calibre_files=True):
#Bibtex doesn't like UTF-8 but keep unicode until writing
#Define starting chain or if book valid strict and not book return a Fail string
@ -360,8 +372,13 @@ class BIBTEX(CatalogPlugin): # {{{
bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item))
elif field == 'formats' :
item = u', '.join([format.rpartition('.')[2].lower() for format in item])
bibtex_entry.append(u'formats = "%s"' % item)
#Add file path if format is selected
formats = [format.rpartition('.')[2].lower() for format in item]
bibtex_entry.append(u'formats = "%s"' % u', '.join(formats))
if calibre_files:
files = [u':%s:%s' % (format, format.rpartition('.')[2].upper())\
for format in item]
bibtex_entry.append(u'files = "%s"' % u', '.join(files))
elif field == 'series_index' :
bibtex_entry.append(u'volume = "%s"' % int(item))
@ -511,31 +528,40 @@ class BIBTEX(CatalogPlugin): # {{{
else :
citation_bibtex= opts.impcit
#Check add file entry and go to default in case of bad CLI
if isinstance(opts.addfiles, (StringType, UnicodeType)) :
if opts.addfiles == 'False' :
addfiles_bibtex = False
elif opts.addfiles == 'True' :
addfiles_bibtex = True
else :
log(" WARNING: incorrect --add-files-path, revert to default")
addfiles_bibtex= True
else :
addfiles_bibtex = opts.addfiles
#Preprocess for error and light correction
template_citation = preprocess_template(opts.bib_cit)
#Open output and write entries
outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)
with codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)\
as outfile:
#File header
nb_entries = len(data)
#check in book strict if all is ok else throw a warning into log
if bib_entry == 'book' :
nb_books = len(filter(check_entry_book_valid, data))
if nb_books < nb_entries :
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
nb_entries = nb_books
#File header
nb_entries = len(data)
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
#check in book strict if all is ok else throw a warning into log
if bib_entry == 'book' :
nb_books = len(filter(check_entry_book_valid, data))
if nb_books < nb_entries :
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
nb_entries = nb_books
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
for entry in data:
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
bibtexc, citation_bibtex))
outfile.close()
for entry in data:
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
bibtexc, citation_bibtex, addfiles_bibtex))
# }}}
class EPUB_MOBI(CatalogPlugin):

View File

@ -21,7 +21,7 @@ from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade
from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.library.sqlite import connect, IntegrityError
from calibre.library.prefs import DBPrefs
from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import Metadata
@ -2796,82 +2796,3 @@ books_series_link feeds
yield id, title, script
def check_integrity(self, callback):
callback(0., _('Checking SQL integrity...'))
self.clean()
user_version = self.user_version
sql = '\n'.join(self.conn.dump())
self.conn.close()
dest = self.dbpath+'.tmp'
if os.path.exists(dest):
os.remove(dest)
conn = None
try:
ndb = DBThread(dest, None)
ndb.connect()
conn = ndb.conn
conn.execute('create table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
conn.commit()
conn.executescript(sql)
conn.commit()
conn.execute('pragma user_version=%d'%user_version)
conn.commit()
conn.execute('drop table temp_sequence')
conn.commit()
conn.close()
except:
if conn is not None:
try:
conn.close()
except:
pass
if os.path.exists(dest):
os.remove(dest)
raise
else:
shutil.copyfile(dest, self.dbpath)
self.connect()
self.initialize_dynamic()
self.refresh()
if os.path.exists(dest):
os.remove(dest)
callback(0.1, _('Checking for missing files.'))
bad = {}
us = self.data.universal_set()
total = float(len(us))
for i, id in enumerate(us):
formats = self.data.get(id, self.FIELD_MAP['formats'], row_is_id=True)
if not formats:
formats = []
else:
formats = [x.lower() for x in formats.split(',')]
actual_formats = self.formats(id, index_is_id=True)
if not actual_formats:
actual_formats = []
else:
actual_formats = [x.lower() for x in actual_formats.split(',')]
for fmt in formats:
if fmt in actual_formats:
continue
if id not in bad:
bad[id] = []
bad[id].append(fmt)
has_cover = self.data.get(id, self.FIELD_MAP['cover'],
row_is_id=True)
if has_cover and self.cover(id, index_is_id=True, as_path=True) is None:
if id not in bad:
bad[id] = []
bad[id].append('COVER')
callback(0.1+0.9*(1+i)/total, _('Checked id') + ' %d'%id)
for id in bad:
for fmt in bad[id]:
if fmt != 'COVER':
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, fmt.upper()))
else:
self.conn.execute('UPDATE books SET has_cover=0 WHERE id=?', (id,))
self.conn.commit()
self.refresh_ids(list(bad.keys()))
return bad

View File

@ -17,6 +17,7 @@ class DBPrefs(dict):
dict.__init__(self)
self.db = db
self.defaults = {}
self.disable_setting = False
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
try:
val = self.raw_to_object(val)
@ -45,6 +46,8 @@ class DBPrefs(dict):
self.db.conn.commit()
def __setitem__(self, key, val):
if self.disable_setting:
return
raw = self.to_raw(val)
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,

View File

@ -107,6 +107,7 @@ My device is not being detected by |app|?
Follow these steps to find the problem:
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
* If you are connecting an Apple iDevice (iPad, iPod Touch, iPhone), use the 'Connect to iTunes' method in the 'Getting started' instructions in `Calibre + Apple iDevices: Start here <http://www.mobileread.com/forums/showthread.php?t=118559>`_.
* Make sure you are running the latest version of |app|. The latest version can always be downloaded from `the calibre website <http://calibre-ebook.com/download>`_.
* Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is.
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
@ -224,13 +225,12 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
You wills ee a list of books in Safari, just click on the epub link for whichever book you want to read, Safari will then prompt you to open it with iBooks.
With the USB cable
With the USB cable + iTunes
^^^^^^^^^^^^^^^^^^^^^^^^^^^
As of |app| version 0.7.0, you can plug your iDevice into the computer using its charging cable, and |app| will detect it and show you a list of books on the device. You can then use the *Send to device button* to send books directly to iBooks on the device. Note that you must have at least iOS 4 installed on your iPhone/iTouch for this to work.
Use the 'Connect to iTunes' method in the 'Getting started' instructions in `Calibre + Apple iDevices: Start here <http://www.mobileread.com/forums/showthread.php?t=118559>`_.
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
For more details on how this works, see `this forum post <http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1>`_.
This method only works on Windows XP and higher, and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
How do I use |app| with my Android phone?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff