diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index a08f0417ee..79dc659f34 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -45,6 +45,13 @@ def to_unicode(raw, encoding='utf-8', errors='strict'): return raw return raw.decode(encoding, errors) +def patheq(p1, p2): + p = os.path + d = lambda x : p.normcase(p.normpath(p.realpath(p.normpath(x)))) + if not p1 or not p2: + return False + return d(p1) == d(p2) + def unicode_path(path, abs=False): if not isinstance(path, unicode): path = path.decode(sys.getfilesystemencoding()) diff --git a/src/calibre/gui2/convert/page_setup.py b/src/calibre/gui2/convert/page_setup.py index 0d2ce91dd1..3f59537db0 100644 --- a/src/calibre/gui2/convert/page_setup.py +++ b/src/calibre/gui2/convert/page_setup.py @@ -35,7 +35,7 @@ class PageSetupWidget(Widget, Ui_Form): TITLE = _('Page Setup') def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'lrf_output', + Widget.__init__(self, parent, 'page_setup', ['margin_top', 'margin_left', 'margin_right', 'margin_bottom', 'input_profile', 'output_profile'] ) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 8b1e61f546..0493ce73aa 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -1,7 +1,6 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, re, time, textwrap, sys, cStringIO -from binascii import hexlify, unhexlify +import os, re, time, textwrap from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \ QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \ @@ -12,7 +11,6 @@ from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \ from calibre.constants import islinux, iswindows from calibre.gui2.dialogs.config_ui import Ui_Dialog -from calibre.gui2.dialogs.test_email_ui import Ui_Dialog as TE_Dialog from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \ ALL_COLUMNS, NONE, info_dialog, choose_files from calibre.utils.config import prefs @@ -202,34 +200,6 @@ class CategoryModel(QStringListModel): return self.icons[index.row()] return QStringListModel.data(self, index, role) -class TestEmail(QDialog, TE_Dialog): - - def __init__(self, accounts, parent): - QDialog.__init__(self, parent) - TE_Dialog.__init__(self) - self.setupUi(self) - opts = smtp_prefs().parse() - self.test_func = parent.test_email_settings - self.connect(self.test_button, SIGNAL('clicked(bool)'), self.test) - self.from_.setText(unicode(self.from_.text())%opts.from_) - if accounts: - self.to.setText(list(accounts.keys())[0]) - if opts.relay_host: - self.label.setText(_('Using: %s:%s@%s:%s and %s encryption')% - (opts.relay_username, unhexlify(opts.relay_password), - opts.relay_host, opts.relay_port, opts.encryption)) - - def test(self): - self.log.setPlainText(_('Sending...')) - self.test_button.setEnabled(False) - try: - tb = self.test_func(unicode(self.to.text())) - if not tb: - tb = _('Mail successfully sent') - self.log.setPlainText(tb) - finally: - self.test_button.setEnabled(True) - class EmailAccounts(QAbstractTableModel): def __init__(self, accounts): @@ -477,32 +447,19 @@ class ConfigDialog(QDialog, Ui_Dialog): self.stackedWidget.insertWidget(2, self.conversion_options) def setup_email_page(self): - opts = smtp_prefs().parse() - if opts.from_: - self.email_from.setText(opts.from_) + def x(): + if self._email_accounts.account_order: + return self._email_accounts.account_order[0] + self.send_email_widget.initialize(x) + opts = self.send_email_widget.smtp_opts self._email_accounts = EmailAccounts(opts.accounts) self.email_view.setModel(self._email_accounts) - if opts.relay_host: - self.relay_host.setText(opts.relay_host) - self.relay_port.setValue(opts.relay_port) - if opts.relay_username: - self.relay_username.setText(opts.relay_username) - if opts.relay_password: - self.relay_password.setText(unhexlify(opts.relay_password)) - (self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True) - self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'), - self.create_gmail_relay) - self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'), - lambda - state:self.relay_password.setEchoMode(self.relay_password.Password if - state == 0 else self.relay_password.Normal)) + self.connect(self.email_add, SIGNAL('clicked(bool)'), self.add_email_account) self.connect(self.email_make_default, SIGNAL('clicked(bool)'), lambda c: self._email_accounts.make_default(self.email_view.currentIndex())) self.email_view.resizeColumnsToContents() - self.connect(self.test_email_button, SIGNAL('clicked(bool)'), - self.test_email) self.connect(self.email_remove, SIGNAL('clicked()'), self.remove_email_account) @@ -516,68 +473,14 @@ class ConfigDialog(QDialog, Ui_Dialog): idx = self.email_view.currentIndex() self._email_accounts.remove(idx) - def create_gmail_relay(self, *args): - self.relay_username.setText('@gmail.com') - self.relay_password.setText('') - self.relay_host.setText('smtp.gmail.com') - self.relay_port.setValue(587) - self.relay_tls.setChecked(True) - - info_dialog(self, _('Finish gmail setup'), - _('Dont forget to enter your gmail username and password')).exec_() - self.relay_username.setFocus(Qt.OtherFocusReason) - self.relay_username.setCursorPosition(0) - def set_email_settings(self): - from_ = unicode(self.email_from.text()).strip() - if self._email_accounts.accounts and not from_: - error_dialog(self, _('Bad configuration'), - _('You must set the From email address')).exec_() - return False - username = unicode(self.relay_username.text()).strip() - password = unicode(self.relay_password.text()).strip() - host = unicode(self.relay_host.text()).strip() - if host and not (username and password): - error_dialog(self, _('Bad configuration'), - _('You must set the username and password for ' - 'the mail server.')).exec_() + to_set = bool(self._email_accounts.accounts) + if not self.send_email_widget.set_email_settings(to_set): return False conf = smtp_prefs() - conf.set('from_', from_) conf.set('accounts', self._email_accounts.accounts) - conf.set('relay_host', host if host else None) - conf.set('relay_port', self.relay_port.value()) - conf.set('relay_username', username if username else None) - conf.set('relay_password', hexlify(password)) - conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL') return True - def test_email(self, *args): - if self.set_email_settings(): - TestEmail(self._email_accounts.accounts, self).exec_() - - def test_email_settings(self, to): - opts = smtp_prefs().parse() - from calibre.utils.smtp import sendmail, create_mail - buf = cStringIO.StringIO() - oout, oerr = sys.stdout, sys.stderr - sys.stdout = sys.stderr = buf - tb = None - try: - msg = create_mail(opts.from_, to, 'Test mail from calibre', - 'Test mail from calibre') - sendmail(msg, from_=opts.from_, to=[to], - verbose=3, timeout=30, relay=opts.relay_host, - username=opts.relay_username, - password=unhexlify(opts.relay_password), - encryption=opts.encryption, port=opts.relay_port) - except: - import traceback - tb = traceback.format_exc() - tb += '\n\nLog:\n' + buf.getvalue() - finally: - sys.stdout, sys.stderr = oout, oerr - return tb def add_plugin(self): path = unicode(self.plugin_path.text()) @@ -722,7 +625,7 @@ class ConfigDialog(QDialog, Ui_Dialog): def browse(self): dir = choose_dir(self, 'database location dialog', - _('Select database location')) + _('Select location for books')) if dir: self.location.setText(dir) diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 0ee6629f91..90a53364ca 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -541,38 +541,7 @@ - - - - calibre can send your books to you (or your reader) by email - - - true - - - - - - - - Send email &from: - - - email_from - - - - - - - <p>This is what will be present in the From: field of emails sent by calibre.<br> Set it to your email address - - - - - - @@ -640,195 +609,18 @@ - - - - <p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services. + + + + calibre can send your books to you (or your reader) by email - - Mail &Server + + true - - - - - calibre can <b>optionally</b> use a server to send mail - - - true - - - - - - - &Hostname: - - - relay_host - - - - - - - The hostname of your mail server. For e.g. smtp.gmail.com - - - - - - - - - &Port: - - - relay_port - - - - - - - The port your mail server listens for connections on. The default is 25 - - - 1 - - - 65555 - - - 25 - - - - - - - - - &Username: - - - relay_username - - - - - - - Your username on the mail server - - - - - - - &Password: - - - relay_password - - - - - - - Your password on the mail server - - - QLineEdit::Password - - - - - - - &Show - - - - - - - &Encryption: - - - relay_tls - - - - - - - Use TLS encryption when connecting to the mail server. This is the most common. - - - &TLS - - - true - - - - - - - Use SSL encryption when connecting to the mail server. - - - &SSL - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - Use Gmail - - - - :/images/gmail_logo.png:/images/gmail_logo.png - - - - 48 - 48 - - - - Qt::ToolButtonTextUnderIcon - - - - - - - &Test email - - - - + + @@ -1062,7 +854,8 @@ - If you want to use the content server to access your ebook collection on your iphone with Stanza, you will need to add the URL http://myhostname:8080/stanza as a new catalog in the stanza reader on your iphone. Here myhostname should be the fully qualified hostname or the IP address of this computer. + <p>Remember to leave calibre running as the server only runs as long as calibre is running. +<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on. true @@ -1216,6 +1009,14 @@ + + + SendEmail + QWidget +
calibre/gui2/wizard/send_email.h
+ 1 +
+
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 03c5f1dbdf..af04207863 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -11,11 +11,11 @@ from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ QToolButton, QDialog, QDesktopServices, QFileDialog, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ - QProgressDialog, QMessageBox, QStackedLayout + QMessageBox, QStackedLayout from PyQt4.QtSvg import QSvgRenderer from calibre import __version__, __appname__, sanitize_file_name, \ - iswindows, isosx, prints + iswindows, isosx, prints, patheq from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ @@ -27,6 +27,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ available_width, GetMetadata from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror from calibre.gui2.widgets import ProgressIndicator +from calibre.gui2.wizard import move_library from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.update import CheckForUpdates from calibre.gui2.main_window import MainWindow, option_parser as _option_parser @@ -297,6 +298,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single) self.convert_menu = cm + pm = QMenu() + pm.addAction(self.action_preferences) + pm.addAction(_('Run welcome wizard')) + self.connect(pm.actions()[1], SIGNAL('triggered(bool)'), + self.run_wizard) + self.action_preferences.setMenu(pm) + self.preferences_menu = pm + self.tool_bar.widgetForAction(self.action_news).\ setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.widgetForAction(self.action_edit).\ @@ -311,6 +320,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.widgetForAction(self.action_view).\ setPopupMode(QToolButton.MenuButtonPopup) + self.tool_bar.widgetForAction(self.action_preferences).\ + setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) self.connect(self.preferences_action, SIGNAL('triggered(bool)'), @@ -1376,56 +1387,27 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.save_menu.actions()[2].setText( _('Save only %s format to disk')% prefs['output_format'].upper()) - if self.library_path != d.database_location: - try: - newloc = d.database_location - if not os.path.exists(os.path.join(newloc, 'metadata.db')): - if os.access(self.library_path, os.R_OK): - pd = QProgressDialog('', '', 0, 100, self) - pd.setWindowModality(Qt.ApplicationModal) - pd.setCancelButton(None) - pd.setWindowTitle(_('Copying database')) - pd.show() - self.status_bar.showMessage( - _('Copying library to ')+newloc) - self.setCursor(Qt.BusyCursor) - self.library_view.setEnabled(False) - self.library_view.model().db.move_library_to( - newloc, pd) - else: - try: - db = LibraryDatabase2(newloc) - self.library_view.set_database(db) - except Exception, err: - traceback.print_exc() - d = error_dialog(self, _('Invalid database'), - _('

An invalid database already exists at ' - '%s, delete it before trying to move the ' - 'existing database.
Error: %s')%(newloc, - str(err))) - d.exec_() - self.library_path = \ - self.library_view.model().db.library_path - prefs['library_path'] = self.library_path - except Exception, err: - traceback.print_exc() - d = error_dialog(self, _('Could not move database'), - unicode(err)) - d.exec_() - finally: - self.unsetCursor() - self.library_view.setEnabled(True) - self.status_bar.clearMessage() - self.search.clear_to_help() - self.status_bar.reset_info() - self.library_view.sortByColumn(3, Qt.DescendingOrder) - self.library_view.resizeRowsToContents() if hasattr(d, 'directories'): set_sidebar_directories(d.directories) self.library_view.model().read_config() self.create_device_menu() + if not patheq(self.library_path, d.database_location): + newloc = d.database_location + move_library(self.library_path, newloc, self, + self.library_moved) + + + def library_moved(self, newloc): + if newloc is None: return + db = LibraryDatabase2(newloc) + self.library_view.set_database(db) + self.status_bar.clearMessage() + self.search.clear_to_help() + self.status_bar.reset_info() + self.library_view.sortByColumn(3, Qt.DescendingOrder) + ############################################################################ ################################ Book info ################################# @@ -1652,6 +1634,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.hide() return True + def run_wizard(self, *args): + if self.confirm_quit(): + self.run_wizard_b4_shutdown = True + self.restart_after_quit = True + try: + self.shutdown(write_settings=False) + except: + pass + QApplication.instance().quit() + + def closeEvent(self, e): self.write_settings() @@ -1726,12 +1719,19 @@ def init_qt(args): def run_gui(opts, args, actions, listener, app): initialize_file_icon_provider() + if not dynamic.get('welcome_wizard_was_run', False): + from calibre.gui2.wizard import wizard + wizard().exec_() + dynamic.set('welcome_wizard_was_run', True) main = Main(listener, opts, actions) sys.excepthook = main.unhandled_exception if len(args) > 1: args[1] = os.path.abspath(args[1]) main.add_filesystem_book(args[1]) ret = app.exec_() + if getattr(main, 'run_wizard_b4_shutdown', False): + from calibre.gui2.wizard import wizard + wizard().exec_() if getattr(main, 'restart_after_quit', False): e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0] print 'Restarting with:', e, sys.argv diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py new file mode 100644 index 0000000000..3e18a638aa --- /dev/null +++ b/src/calibre/gui2/wizard/__init__.py @@ -0,0 +1,500 @@ +#!/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' + +import os, traceback, re +from Queue import Empty, Queue +from contextlib import closing + + +from PyQt4.Qt import QWizard, QWizardPage, QPixmap, Qt, QAbstractListModel, \ + QVariant, QItemSelectionModel, SIGNAL, QObject, QTimer +from calibre import __appname__, patheq +from calibre.library.database2 import LibraryDatabase2 +from calibre.library.move import MoveLibrary +from calibre.resources import server_resources +from calibre.gui2.wizard.send_email import smtp_prefs +from calibre.gui2.wizard.device_ui import Ui_WizardPage as DeviceUI +from calibre.gui2.wizard.library_ui import Ui_WizardPage as LibraryUI +from calibre.gui2.wizard.finish_ui import Ui_WizardPage as FinishUI +from calibre.gui2.wizard.kindle_ui import Ui_WizardPage as KindleUI +from calibre.gui2.wizard.stanza_ui import Ui_WizardPage as StanzaUI + +from calibre.utils.config import dynamic, prefs +from calibre.gui2 import NONE, choose_dir, error_dialog +from calibre.gui2.dialogs.progress import ProgressDialog + +class Device(object): + + output_profile = 'default' + output_format = 'EPUB' + name = _('Default') + manufacturer = _('Default') + id = 'default' + + @classmethod + def set_output_profile(cls): + if cls.output_profile: + from calibre.ebooks.conversion.config import load_defaults, save_defaults + recs = load_defaults('page_setup') + recs['output_profile'] = cls.output_profile + save_defaults('page_setup', recs) + + @classmethod + def set_output_format(cls): + if cls.output_format: + prefs.set('output_format', cls.output_format) + + @classmethod + def commit(cls): + cls.set_output_profile() + cls.set_output_format() + +class Kindle(Device): + + output_profile = 'kindle' + output_format = 'MOBI' + name = 'Kindle 1 or 2' + manufacturer = 'Amazon' + id = 'kindle' + +class Sony500(Device): + + output_profile = 'sony' + name = 'SONY PRS 500' + output_format = 'LRF' + manufacturer = 'SONY' + id = 'prs500' + +class Sony505(Sony500): + + output_format = 'EPUB' + name = 'SONY PRS 505/700' + id = 'prs505' + +class CybookG3(Device): + + name = 'Cybook Gen 3' + output_format = 'MOBI' + output_profile = 'cybookg3' + manufacturer = 'Booken' + id = 'cybookg3' + +class BeBook(Device): + + name = 'BeBook or BeBook Mini' + output_format = 'EPUB' + output_profile = 'sony' + manufacturer = 'Endless Ideas' + id = 'bebook' + +class iPhone(Device): + + name = 'iPhone/iTouch + Stanza' + output_format = 'EPUB' + manufacturer = 'Apple' + id = 'iphone' + +class Hanlin(Device): + + name = 'Hanlin V3' + output_format = 'MOBI' + output_profile = 'hanlinv3' + manufacturer = 'Hanlin' + id = 'hanlinv3' + +def get_devices(): + for x in globals().values(): + if isinstance(x, type) and issubclass(x, Device): + yield x + +def get_manufacturers(): + mans = set([]) + for x in get_devices(): + mans.add(x.manufacturer) + mans.remove(_('Default')) + return [_('Default')] + sorted(mans) + +def get_devices_of(manufacturer): + ans = [d for d in get_devices() if d.manufacturer == manufacturer] + return sorted(ans, cmp=lambda x,y:cmp(x.name, y.name)) + +class ManufacturerModel(QAbstractListModel): + + def __init__(self): + QAbstractListModel.__init__(self) + self.manufacturers = get_manufacturers() + + def rowCount(self, p): + return len(self.manufacturers) + + def columnCount(self, p): + return 1 + + def data(self, index, role): + if role == Qt.DisplayRole: + return QVariant(self.manufacturers[index.row()]) + if role == Qt.UserRole: + return self.manufacturers[index.row()] + return NONE + + def index_of(self, man): + for i, x in enumerate(self.manufacturers): + if x == man: + return self.index(i) + +class DeviceModel(QAbstractListModel): + + def __init__(self, manufacturer): + QAbstractListModel.__init__(self) + self.devices = get_devices_of(manufacturer) + + def rowCount(self, p): + return len(self.devices) + + def columnCount(self, p): + return 1 + + def data(self, index, role): + if role == Qt.DisplayRole: + return QVariant(self.devices[index.row()].name) + if role == Qt.UserRole: + return self.devices[index.row()] + return NONE + + def index_of(self, dev): + for i, device in enumerate(self.devices): + if device is dev: + return self.index(i) + +class KindlePage(QWizardPage, KindleUI): + + ID = 3 + + def __init__(self): + QWizardPage.__init__(self) + self.setupUi(self) + + def initializePage(self): + opts = smtp_prefs().parse() + for x in opts.accounts.keys(): + if x.strip().endswith('@kindle.com'): + self.to_address.setText(x) + def x(): + t = unicode(self.to_address.text()) + if t.strip(): + return t.strip() + + self.send_email_widget.initialize(x) + + def commit(self): + x = unicode(self.to_address.text()).strip() + parts = x.split('@') + if len(parts) < 2 or not parts[0]: return + + if self.send_email_widget.set_email_settings(True): + conf = smtp_prefs() + accounts = conf.get('accounts', {}) + if not accounts: accounts = {} + for y in accounts.values(): + y[2] = False + accounts[x] = ['AZW, MOBI, TPZ, PRC, AZW1', True, True] + conf.set('accounts', accounts) + + def nextId(self): + return FinishPage.ID + +class StanzaPage(QWizardPage, StanzaUI): + + ID = 5 + + def __init__(self): + QWizardPage.__init__(self) + self.setupUi(self) + self.connect(self.content_server, SIGNAL('stateChanged(int)'), self.set_port) + + def initializePage(self): + from calibre.gui2 import config + yes = config['autolaunch_server'] + self.content_server.setChecked(yes) + self.set_port() + + def nextId(self): + return FinishPage.ID + + def commit(self): + p = self.set_port() + if p is not None: + from calibre.library import server_config + c = server_config() + c.set('port', p) + + + def set_port(self, *args): + if not self.content_server.isChecked(): return + import socket + s = socket.socket() + with closing(s): + for p in range(8080, 8100): + try: + s.bind(('0.0.0.0', p)) + t = unicode(self.instructions.text()) + t = re.sub(r':\d+', ':'+str(p), t) + self.instructions.setText(t) + return p + except: + continue + + + + +class DevicePage(QWizardPage, DeviceUI): + + ID = 2 + + def __init__(self): + QWizardPage.__init__(self) + self.setupUi(self) + self.registerField("manufacturer", self.manufacturer_view) + self.registerField("device", self.device_view) + + def initializePage(self): + self.man_model = ManufacturerModel() + self.manufacturer_view.setModel(self.man_model) + previous = dynamic.get('welcome_wizard_device', False) + if previous: + previous = [x for x in get_devices() if \ + x.id == previous] + if not previous: + previous = [Device] + previous = previous[0] + else: + previous = Device + idx = self.man_model.index_of(previous.manufacturer) + if idx is None: + idx = self.man_model.index_of(Device.manufacturer) + previous = Device + self.manufacturer_view.selectionModel().select(idx, + QItemSelectionModel.Select) + self.dev_model = DeviceModel(self.man_model.data(idx, Qt.UserRole)) + idx = self.dev_model.index_of(previous) + self.device_view.setModel(self.dev_model) + self.device_view.selectionModel().select(idx, + QItemSelectionModel.Select) + self.connect(self.manufacturer_view.selectionModel(), + SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), + self.manufacturer_changed) + + def manufacturer_changed(self, current, previous): + new = list(current.indexes())[0] + man = self.man_model.data(new, Qt.UserRole) + self.dev_model = DeviceModel(man) + self.device_view.setModel(self.dev_model) + self.device_view.selectionModel().select(self.dev_model.index(0), + QItemSelectionModel.Select) + + def commit(self): + idx = list(self.device_view.selectionModel().selectedIndexes())[0] + dev = self.dev_model.data(idx, Qt.UserRole) + dev.commit() + dynamic.set('welcome_wizard_device', dev.id) + + def nextId(self): + idx = list(self.device_view.selectionModel().selectedIndexes())[0] + dev = self.dev_model.data(idx, Qt.UserRole) + if dev is Kindle: + return KindlePage.ID + if dev is iPhone: + return StanzaPage.ID + return FinishPage.ID + +class MoveMonitor(QObject): + + def __init__(self, worker, rq, callback, parent): + QObject.__init__(self, parent) + self.worker = worker + self.rq = rq + self.callback = callback + self.parent = parent + + self.worker.start() + self.dialog = ProgressDialog(_('Moving library...'), '', + max=self.worker.total, parent=parent) + self.dialog.button_box.setDisabled(True) + self.dialog.setModal(True) + self.dialog.show() + self.timer = QTimer(self) + self.connect(self.timer, SIGNAL('timeout()'), self.check) + self.timer.start(200) + + def check(self): + if self.worker.is_alive(): + self.update() + else: + self.timer.stop() + self.dialog.hide() + if self.worker.failed: + error_dialog(self.parent, _('Failed to move library'), + _('Failed to move library'), self.worker.details, show=True) + return self.callback(None) + else: + return self.callback(self.worker.to) + + def update(self): + try: + title = self.rq.get_nowait()[-1] + self.dialog.value += 1 + self.dialog.set_msg(_('Copied') + ' '+title) + except Empty: + pass + + +class Callback(object): + + def __init__(self, callback): + self.callback = callback + + def __call__(self, newloc): + if newloc is not None: + prefs['library_path'] = newloc + self.callback(newloc) + +_mm = None +def move_library(oldloc, newloc, parent, callback_on_complete): + callback = Callback(callback_on_complete) + try: + if not os.path.exists(os.path.join(newloc, 'metadata.db')): + if oldloc and os.access(os.path.join(oldloc, 'metadata.db'), os.R_OK): + # Move old library to new location + try: + db = LibraryDatabase2(oldloc) + except: + return move_library(None, newloc, parent, + callback) + else: + rq = Queue() + m = MoveLibrary(oldloc, newloc, db.data.count(), rq) + global _mm + _mm = MoveMonitor(m, rq, callback, parent) + return + else: + # Create new library at new location + db = LibraryDatabase2(newloc) + callback(newloc) + return + + # Try to load existing library at new location + try: + ndb = LibraryDatabase2(newloc) + except Exception, err: + det = traceback.format_exc() + error_dialog(parent, _('Invalid database'), + _('

An invalid library already exists at ' + '%s, delete it before trying to move the ' + 'existing library.
Error: %s')%(newloc, + str(err)), det, show=True) + callback(None) + return + else: + callback(newloc) + return + except Exception, err: + det = traceback.format_exc() + error_dialog(parent, _('Could not move library'), + unicode(err), det, show=True) + callback(None) + +class LibraryPage(QWizardPage, LibraryUI): + + ID = 1 + + def __init__(self): + QWizardPage.__init__(self) + self.setupUi(self) + self.registerField('library_location', self.location) + self.connect(self.button_change, SIGNAL('clicked()'), self.change) + + def change(self): + dir = choose_dir(self, 'database location dialog', + _('Select location for books')) + if dir: + self.location.setText(dir) + + def initializePage(self): + lp = prefs['library_path'] + if not lp: + lp = os.path.expanduser('~') + self.location.setText(lp) + + def isComplete(self): + lp = unicode(self.location.text()) + return lp and os.path.exists(lp) and os.path.isdir(lp) and os.access(lp, + os.W_OK) + + def commit(self, completed): + oldloc = prefs['library_path'] + newloc = unicode(self.location.text()) + if not patheq(oldloc, newloc): + move_library(oldloc, newloc, self.wizard(), completed) + return True + return False + + def nextId(self): + return DevicePage.ID + +class FinishPage(QWizardPage, FinishUI): + + ID = 4 + + def __init__(self): + QWizardPage.__init__(self) + self.setupUi(self) + + def nextId(self): + return -1 + + + +class Wizard(QWizard): + + def __init__(self, parent): + QWizard.__init__(self, parent) + self.setWindowTitle(__appname__+' '+_('welcome wizard')) + p = QPixmap() + p.loadFromData(server_resources['calibre.png']) + self.setPixmap(self.LogoPixmap, p.scaledToHeight(80, + Qt.SmoothTransformation)) + self.device_page = DevicePage() + self.library_page = LibraryPage() + self.finish_page = FinishPage() + self.kindle_page = KindlePage() + self.stanza_page = StanzaPage() + self.setPage(self.library_page.ID, self.library_page) + self.setPage(self.device_page.ID, self.device_page) + self.setPage(self.finish_page.ID, self.finish_page) + self.setPage(self.kindle_page.ID, self.kindle_page) + self.setPage(self.stanza_page.ID, self.stanza_page) + + self.device_extra_page = None + + def accept(self): + self.device_page.commit() + if not self.library_page.commit(self.completed): + self.completed(None) + + def completed(self, newloc): + return QWizard.accept(self) + +def wizard(parent=None): + w = Wizard(parent) + return w + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + wizard().exec_() + diff --git a/src/calibre/gui2/wizard/device.ui b/src/calibre/gui2/wizard/device.ui new file mode 100644 index 0000000000..27af13b3ed --- /dev/null +++ b/src/calibre/gui2/wizard/device.ui @@ -0,0 +1,75 @@ + + + WizardPage + + + + 0 + 0 + 400 + 300 + + + + Welcome to calibre + + + + :/images/wizard.svg:/images/wizard.svg + + + Welcome to calibre + + + The one stop solution to all your e-book needs. + + + + + + Choose your book reader. This will set the conversion options to produce books optimized for your device. + + + true + + + + + + + &Manufacturers + + + + + + QAbstractItemView::SelectRows + + + + + + + + + + &Devices + + + + + + QAbstractItemView::SelectRows + + + + + + + + + + + + + diff --git a/src/calibre/gui2/wizard/finish.ui b/src/calibre/gui2/wizard/finish.ui new file mode 100644 index 0000000000..b42e8b1c32 --- /dev/null +++ b/src/calibre/gui2/wizard/finish.ui @@ -0,0 +1,108 @@ + + + WizardPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Welcome to calibre + + + The one stop solution to all your e-book needs. + + + + + + <h2>Congratulations!</h2> You have succesfully setup calibre. Press the Finish button to apply your settings. + + + true + + + + + + + Qt::Vertical + + + + 20 + 56 + + + + + + + + <h2>Demo videos</h2>Videos demonstrating the various features of calibre are available <a href="http://calibre.kovidgoyal.net/downloads/videos/">online</a>. + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + Qt::Vertical + + + + 20 + 56 + + + + + + + + <h2>User Manual</h2>A User Manual is also available <a href="http://calibre.kovidgoyal.net/user_manual">online</a>. + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/calibre/gui2/wizard/kindle.ui b/src/calibre/gui2/wizard/kindle.ui new file mode 100644 index 0000000000..7bafc6d618 --- /dev/null +++ b/src/calibre/gui2/wizard/kindle.ui @@ -0,0 +1,80 @@ + + + WizardPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Welcome to calibre + + + The one stop solution to all your e-book needs. + + + + + + <p>calibre can automatically send books by email to your Kindle. To do that you have to setup email delivery below. The easiest way is to setup a free <a href="http://gmail.com">gmail account</a> and click the Use gmail button below. You will also have to register your gmail address in your Amazon account. + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + &Kindle email: + + + to_address + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + SendEmail + QWidget +

calibre/gui2/wizard/send_email.h
+ 1 + + + + + diff --git a/src/calibre/gui2/wizard/library.ui b/src/calibre/gui2/wizard/library.ui new file mode 100644 index 0000000000..d3c93bbd3c --- /dev/null +++ b/src/calibre/gui2/wizard/library.ui @@ -0,0 +1,74 @@ + + + WizardPage + + + + 0 + 0 + 481 + 300 + + + + WizardPage + + + Welcome to calibre + + + The one stop solution to all your e-book needs. + + + + + + Choose a location for your books. When you add books to calibre, they will be stored here: + + + true + + + + + + + true + + + + + + + &Change + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + If you have an existing calibre library, it will be copied to the new location. If a calibre library already exists at the new location, calibre will switch to using it. + + + true + + + + + + + + diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py new file mode 100644 index 0000000000..5650279c15 --- /dev/null +++ b/src/calibre/gui2/wizard/send_email.py @@ -0,0 +1,143 @@ +#!/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' + +import cStringIO, sys +from binascii import hexlify, unhexlify + +from PyQt4.Qt import QWidget, SIGNAL, QDialog, Qt + +from calibre.gui2.wizard.send_email_ui import Ui_Form +from calibre.utils.smtp import config as smtp_prefs +from calibre.gui2.dialogs.test_email_ui import Ui_Dialog as TE_Dialog +from calibre.gui2 import error_dialog, info_dialog + +class TestEmail(QDialog, TE_Dialog): + + def __init__(self, pa, parent): + QDialog.__init__(self, parent) + TE_Dialog.__init__(self) + self.setupUi(self) + opts = smtp_prefs().parse() + self.test_func = parent.test_email_settings + self.connect(self.test_button, SIGNAL('clicked(bool)'), self.test) + self.from_.setText(unicode(self.from_.text())%opts.from_) + if pa: + self.to.setText(pa) + if opts.relay_host: + self.label.setText(_('Using: %s:%s@%s:%s and %s encryption')% + (opts.relay_username, unhexlify(opts.relay_password), + opts.relay_host, opts.relay_port, opts.encryption)) + + def test(self): + self.log.setPlainText(_('Sending...')) + self.test_button.setEnabled(False) + try: + tb = self.test_func(unicode(self.to.text())) + if not tb: + tb = _('Mail successfully sent') + self.log.setPlainText(tb) + finally: + self.test_button.setEnabled(True) + + +class SendEmail(QWidget, Ui_Form): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.setupUi(self) + + def initialize(self, preferred_to_address): + self.preferred_to_address = preferred_to_address + opts = smtp_prefs().parse() + self.smtp_opts = opts + if opts.from_: + self.email_from.setText(opts.from_) + if opts.relay_host: + self.relay_host.setText(opts.relay_host) + self.relay_port.setValue(opts.relay_port) + if opts.relay_username: + self.relay_username.setText(opts.relay_username) + if opts.relay_password: + self.relay_password.setText(unhexlify(opts.relay_password)) + (self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True) + self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'), + self.create_gmail_relay) + self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'), + lambda + state:self.relay_password.setEchoMode(self.relay_password.Password if + state == 0 else self.relay_password.Normal)) + self.connect(self.test_email_button, SIGNAL('clicked(bool)'), + self.test_email) + + + def test_email(self, *args): + pa = self.preferred_to_address() + to_set = pa is not None + if self.set_email_settings(to_set): + TestEmail(pa, self).exec_() + + def test_email_settings(self, to): + opts = smtp_prefs().parse() + from calibre.utils.smtp import sendmail, create_mail + buf = cStringIO.StringIO() + oout, oerr = sys.stdout, sys.stderr + sys.stdout = sys.stderr = buf + tb = None + try: + msg = create_mail(opts.from_, to, 'Test mail from calibre', + 'Test mail from calibre') + sendmail(msg, from_=opts.from_, to=[to], + verbose=3, timeout=30, relay=opts.relay_host, + username=opts.relay_username, + password=unhexlify(opts.relay_password), + encryption=opts.encryption, port=opts.relay_port) + except: + import traceback + tb = traceback.format_exc() + tb += '\n\nLog:\n' + buf.getvalue() + finally: + sys.stdout, sys.stderr = oout, oerr + return tb + + def create_gmail_relay(self, *args): + self.relay_username.setText('@gmail.com') + self.relay_password.setText('') + self.relay_host.setText('smtp.gmail.com') + self.relay_port.setValue(587) + self.relay_tls.setChecked(True) + + info_dialog(self, _('Finish gmail setup'), + _('Dont forget to enter your gmail username and password')).exec_() + self.relay_username.setFocus(Qt.OtherFocusReason) + self.relay_username.setCursorPosition(0) + + def set_email_settings(self, to_set): + from_ = unicode(self.email_from.text()).strip() + if to_set and not from_: + error_dialog(self, _('Bad configuration'), + _('You must set the From email address')).exec_() + return False + username = unicode(self.relay_username.text()).strip() + password = unicode(self.relay_password.text()).strip() + host = unicode(self.relay_host.text()).strip() + if host and not (username and password): + error_dialog(self, _('Bad configuration'), + _('You must set the username and password for ' + 'the mail server.')).exec_() + return False + conf = smtp_prefs() + conf.set('from_', from_) + conf.set('relay_host', host if host else None) + conf.set('relay_port', self.relay_port.value()) + conf.set('relay_username', username if username else None) + conf.set('relay_password', hexlify(password)) + conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL') + return True + + + diff --git a/src/calibre/gui2/wizard/send_email.ui b/src/calibre/gui2/wizard/send_email.ui new file mode 100644 index 0000000000..3802d7f451 --- /dev/null +++ b/src/calibre/gui2/wizard/send_email.ui @@ -0,0 +1,234 @@ + + + Form + + + + 0 + 0 + 585 + 238 + + + + Form + + + + + + + + Send email &from: + + + email_from + + + + + + + <p>This is what will be present in the From: field of emails sent by calibre.<br> Set it to your email address + + + + + + + + + <p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services. + + + Mail &Server + + + + + + calibre can <b>optionally</b> use a server to send mail + + + true + + + + + + + &Hostname: + + + relay_host + + + + + + + The hostname of your mail server. For e.g. smtp.gmail.com + + + + + + + + + &Port: + + + relay_port + + + + + + + The port your mail server listens for connections on. The default is 25 + + + 1 + + + 65555 + + + 25 + + + + + + + + + &Username: + + + relay_username + + + + + + + Your username on the mail server + + + + + + + &Password: + + + relay_password + + + + + + + Your password on the mail server + + + QLineEdit::Password + + + + + + + &Show + + + + + + + &Encryption: + + + relay_tls + + + + + + + Use TLS encryption when connecting to the mail server. This is the most common. + + + &TLS + + + true + + + + + + + Use SSL encryption when connecting to the mail server. + + + &SSL + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Use Gmail + + + + :/images/gmail_logo.png:/images/gmail_logo.png + + + + 48 + 48 + + + + Qt::ToolButtonTextUnderIcon + + + + + + + &Test email + + + + + + + + + + + + diff --git a/src/calibre/gui2/wizard/stanza.ui b/src/calibre/gui2/wizard/stanza.ui new file mode 100644 index 0000000000..6dee91f8fc --- /dev/null +++ b/src/calibre/gui2/wizard/stanza.ui @@ -0,0 +1,97 @@ + + + WizardPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Welcome to calibre + + + The one stop solution to all your e-book needs. + + + + + + <p>If you use the <a href="http://www.lexcycle.com/download">Stanza</a> e-book app on your iPhone/iTouch, you can access your calibre book collection directly on the device. To do this you have to turn on the calibre content server. + + + true + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Turn on the &content server + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <p>Remember to leave calibre running as the server only runs as long as calibre is running. +<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9135182258..1b373bf738 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1282,21 +1282,12 @@ class LibraryDatabase2(LibraryDatabase): if notify: self.notify('add', [id]) - def move_library_to(self, newloc, progress=None): - header = _(u'

Copying books to %s

')%newloc + def move_library_to(self, newloc, progress=lambda x: x): books = self.conn.get('SELECT id, path, title FROM books') - if progress is not None: - progress.setValue(0) - progress.setLabelText(header) - QCoreApplication.processEvents() - progress.setAutoReset(False) - progress.setRange(0, len(books)) if not os.path.exists(newloc): os.makedirs(newloc) old_dirs = set([]) for i, book in enumerate(books): - if progress is not None: - progress.setLabelText(header+_(u'Copying %s')%book[2]) path = book[1] if not path: continue @@ -1308,8 +1299,7 @@ class LibraryDatabase2(LibraryDatabase): if os.path.exists(srcdir): shutil.copytree(srcdir, tdir) old_dirs.add(srcdir) - if progress is not None: - progress.setValue(i+1) + progress(book[2]) dbpath = os.path.join(newloc, os.path.basename(self.dbpath)) shutil.copyfile(self.dbpath, dbpath) @@ -1323,10 +1313,6 @@ class LibraryDatabase2(LibraryDatabase): shutil.rmtree(dir) except: pass - if progress is not None: - progress.reset() - progress.hide() - def __iter__(self): for record in self.data._data: diff --git a/src/calibre/library/move.py b/src/calibre/library/move.py new file mode 100644 index 0000000000..d162d962fe --- /dev/null +++ b/src/calibre/library/move.py @@ -0,0 +1,63 @@ +#!/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' + +import time, os +from threading import Thread +from Queue import Empty + +from calibre.library.database2 import LibraryDatabase2 +from calibre.utils.ipc.server import Server +from calibre.utils.ipc.job import ParallelJob + + +def move_library(from_, to, notification = lambda x:x): + time.sleep(1) + old = LibraryDatabase2(from_) + old.move_library_to(to, notification) + return True + +class MoveLibrary(Thread): + + def __init__(self, from_, to, count, result_queue): + Thread.__init__(self) + self.total = count + self.result_queue = result_queue + self.from_ = from_ + self.to = to + self.count = 0 + self.failed = False + self.details = None + + def run(self): + job = ParallelJob('move_library', + 'Move library from %s to %s'%(self.from_, self.to), + lambda x,y:x, + args=[self.from_, self.to]) + server = Server(pool_size=1) + server.add_job(job) + + while not job.is_finished: + time.sleep(0.2) + job.update(consume_notifications=False) + while True: + try: + title = job.notifications.get_nowait()[0] + self.count += 1 + self.result_queue.put((float(self.count)/self.total, title)) + except Empty: + break + + job.update() + server.close() + if not job.result: + self.failed = True + self.details = job.details + + if os.path.exists(job.log_path): + os.remove(job.log_path) + diff --git a/src/calibre/library/static/calibre.png b/src/calibre/library/static/calibre.png index f42e4926ca..871a5e31a8 100644 Binary files a/src/calibre/library/static/calibre.png and b/src/calibre/library/static/calibre.png differ diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py index dd25279071..09b38e4ae1 100644 --- a/src/calibre/trac/plugins/download.py +++ b/src/calibre/trac/plugins/download.py @@ -9,8 +9,8 @@ DEPENDENCIES = [ ('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'), ('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'), ('libusb', '0.1.12', None, None, None), - ('Qt', '4.4.0', 'qt', 'libqt4-core libqt4-gui', 'qt4'), - ('PyQt', '4.4.2', 'PyQt4', 'python-qt4', 'PyQt4'), + ('Qt', '4.5.0', 'qt', 'libqt4-core libqt4-gui', 'qt4'), + ('PyQt', '4.5.0', 'PyQt4', 'python-qt4', 'PyQt4'), ('python-mechanize', '0.1.11', 'dev-python/mechanize', 'python-mechanize', 'python-mechanize'), ('ImageMagick', '6.3.5', 'imagemagick', 'imagemagick', 'ImageMagick'), ('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'), diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index de220340db..0e637a6b55 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -27,6 +27,9 @@ PARALLEL_FUNCS = { 'gui_convert' : ('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'), + 'move_library' : + ('calibre.library.move', 'move_library', 'notification'), + 'read_metadata' : ('calibre.ebooks.metadata.worker', 'read_metadata_', 'notification'), @@ -36,6 +39,8 @@ PARALLEL_FUNCS = { 'write_pdf_metadata' : ('calibre.utils.podofo.__init__', 'set_metadata_', None), + 'write_pdf_first_page' : + ('calibre.utils.podofo.__init__', 'write_first_page_', None), 'save_book' : ('calibre.ebooks.metadata.worker', 'save_book', 'notification'), diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index 0920161316..f956c4ee10 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -398,7 +398,7 @@ class RecursiveFetcher(object): len(re.compile('', re.DOTALL).sub('', dsrc).strip()) == 0: raise ValueError('No content at URL %s'%iurl) if self.encoding is not None: - dsrc = dsrc.decode(self.encoding, 'ignore') + dsrc = dsrc.decode(self.encoding, 'replace') else: dsrc = xml_to_unicode(dsrc, self.verbose)[0] diff --git a/todo b/todo index 2777a54070..bff97ad7e8 100644 --- a/todo +++ b/todo @@ -5,7 +5,5 @@ * Testing framework -* Welcome wizard - * MOBI navigation indexing support