Merge from trunk

This commit is contained in:
Charles Haley 2010-06-13 07:48:19 +01:00
commit 7bf0c77dff
19 changed files with 1639 additions and 1714 deletions

View File

@ -11,11 +11,11 @@ http://www.libero-news.it/
from calibre.web.feeds.news import BasicNewsRecipe
class LiberoNews(BasicNewsRecipe):
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
title = u'Libero'
cover_url = 'http://www.libero-news.it/images/logo.png'
title = u'Libero '
publisher = 'EDITORIALE LIBERO s.r.l 2006'
category = 'News, politics, culture, economy, general interest'

View File

@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe):
language = 'en_AU'
oldest_article = 2
max_articles_per_feed = 10
max_articles_per_feed = 20
remove_javascript = True
no_stylesheets = True
encoding = 'utf8'

View File

@ -30,7 +30,7 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung
0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]},
0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
# Acer
0x502 : { 0x3203 : [0x0100]},
@ -41,10 +41,12 @@ class ANDROID(USBMS):
'be used')
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700']
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE']
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
'PROD_GT-I9000']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD']
OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -76,7 +76,7 @@ class ITUNES(DevicePlugin):
supported_platforms = ['osx','windows']
author = 'GRiker'
#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (0, 5, 0)
version = (0,6,0)
OPEN_FEEDBACK_MESSAGE = _(
'Apple device detected, launching iTunes, please wait ...')
@ -280,7 +280,7 @@ class ITUNES(DevicePlugin):
if self.report_progress is not None:
self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
self._purge_orphans(cached_books)
self._purge_orphans(library_books, cached_books)
elif iswindows:
try:
@ -316,7 +316,7 @@ class ITUNES(DevicePlugin):
if self.report_progress is not None:
self.report_progress(i+1/book_count,
_('%d of %d') % (i+1, book_count))
self._purge_orphans(cached_books)
self._purge_orphans(library_books, cached_books)
finally:
pythoncom.CoUninitialize()
@ -324,9 +324,9 @@ class ITUNES(DevicePlugin):
if self.report_progress is not None:
self.report_progress(1.0, _('finished'))
self.cached_books = cached_books
if DEBUG:
self._dump_booklist(booklist, 'returning from books():')
self._dump_cached_books('returning from books():')
# if DEBUG:
# self._dump_booklist(booklist, 'returning from books():')
# self._dump_cached_books('returning from books():')
return booklist
else:
return []
@ -463,7 +463,7 @@ class ITUNES(DevicePlugin):
else:
# iTunes running, but not connected iPad
if DEBUG:
self.log.info(' self.ejected = True')
self.log.info(' iDevice has been ejected')
self.ejected = True
return False
@ -782,121 +782,6 @@ class ITUNES(DevicePlugin):
# self._dump_cached_books('upload_books()')
self._dump_update_list('upload_books()')
'''
if isosx:
for (i,file) in enumerate(files):
path = self.path_template % (metadata[i].title, metadata[i].author[0])
if self.manual_sync_mode:
# Delete existing from Device|Books, add to self.update_list
# for deletion from booklist[0] during add_books_to_metadata
if path in self.cached_books:
self.update_list.append(self.cached_books[path])
if DEBUG:
self.log.info(" adding '%s' by %s to self.update_list" %
(self.cached_books[path]['title'],self.cached_books[path]['author']))
if DEBUG:
self.log.info( " deleting existing '%s'" % (path))
self._remove_from_iTunes(self.cached_books[path])
if self.manual_sync_mode:
dev_book_added = self._remove_from_device(self.cached_books[path])
# Add to iTunes Library|Books
fpath = file
if getattr(file, 'orig_file_path', None) is not None:
fpath = file.orig_file_path
elif getattr(file, 'name', None) is not None:
fpath = file.name
if isinstance(file,PersistentTemporaryFile) and self.manual_sync_mode:
if DEBUG:
self.log.info(" PTF not added to Library|Books")
else:
added = self.iTunes.add(appscript.mactypes.File(fpath))
if DEBUG:
self.log.info(" file added to Library|Books")
dev_book_added = None
if self.manual_sync_mode:
dev_book_added = self._add_device_book(fpath)
thumb = None
if metadata[i].cover:
try:
# Use cover data as artwork
cover_data = open(metadata[i].cover,'rb')
added.artworks[1].data_.set(cover_data.read())
# Resize for thumb
width = metadata[i].thumbnail[0]
height = metadata[i].thumbnail[1]
im = PILImage.open(metadata[i].cover)
im = im.resize((width, height), PILImage.ANTIALIAS)
of = cStringIO.StringIO()
im.convert('RGB').save(of, 'JPEG')
thumb = of.getvalue()
# Refresh the thumbnail cache
if DEBUG:
self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title)
archive_path = os.path.join(self.cache_dir, "thumbs.zip")
zfw = zipfile.ZipFile(archive_path, mode='a')
thumb_path = path.rpartition('.')[0] + '.jpg'
zfw.writestr(thumb_path, thumb)
zfw.close()
except:
self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0]))
self.log.error("ITUNES.upload_books(): error converting '%s' to thumb for '%s'" % (metadata[i].cover,metadata[i].title))
# Create a new Book
this_book = Book(metadata[i].title, metadata[i].author[0])
try:
this_book.datetime = parse_date(str(added.date_added())).timetuple()
except:
pass
this_book.db_id = None
this_book.device_collections = []
this_book.library_id = added
this_book.path = path
this_book.size = self._get_device_book_size(fpath, added.size())
this_book.thumbnail = thumb
this_book.iTunes_id = added
new_booklist.append(this_book)
# Populate the iTunes metadata
if metadata[i].comments:
added.comment.set(strip_tags.sub('',metadata[i].comments))
added.description.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S'))
added.enabled.set(True)
if metadata[i].rating:
added.rating.set(metadata[i].rating*10)
added.sort_artist.set(metadata[i].author_sort.title())
added.sort_name.set(this_book.title_sorter)
# Set genre from metadata
# iTunes grabs the first dc:subject from the opf metadata,
# But we can manually override with first tag starting with alpha
for tag in metadata[i].tags:
if self._is_alpha(tag[0]):
added.genre.set(tag)
break
# Add new_book to self.cached_paths
self.cached_books[this_book.path] = {
'title': this_book.title,
'author': this_book.author,
'lib_book': added,
'dev_book': dev_book_added
}
# Report progress
if self.report_progress is not None:
self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
'''
if isosx:
for (i,file) in enumerate(files):
path = self.path_template % (metadata[i].title, metadata[i].author[0])
@ -1378,6 +1263,15 @@ class ITUNES(DevicePlugin):
self.log.info(" %s" % file.name)
self.log.info()
def _dump_library_books(self, library_books):
'''
'''
if DEBUG:
self.log.info("\n library_books:")
for book in library_books:
self.log.info(" %s" % book)
self.log.info()
def _dump_update_list(self,header=None):
if header:
msg = '\nself.update_list called from %s' % header
@ -1590,7 +1484,7 @@ class ITUNES(DevicePlugin):
self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind()))
else:
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind()))
self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind()))
device_books.append(book)
elif iswindows:
@ -1619,7 +1513,7 @@ class ITUNES(DevicePlugin):
self.log.info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString))
else:
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString))
self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString))
device_books.append(book)
finally:
@ -1716,11 +1610,11 @@ class ITUNES(DevicePlugin):
if book.location() == appscript.k.missing_value:
library_orphans[path] = book
if DEBUG:
self.log.info(" found calibre orphan '%s' in Library|Books" % book.name())
self.log.info(" found iTunes PTF '%s' in Library|Books" % book.name())
library_books[path] = book
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind()))
self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind()))
else:
if DEBUG:
self.log.info(' no Library playlists')
@ -1730,9 +1624,6 @@ class ITUNES(DevicePlugin):
elif iswindows:
lib = None
# try:
# pythoncom.CoInitialize()
# self.iTunes = win32com.client.Dispatch("iTunes.Application")
for source in self.iTunes.sources:
if source.Kind == self.Sources.index('Library'):
lib = source
@ -1772,16 +1663,14 @@ class ITUNES(DevicePlugin):
if not book.Location:
library_orphans[path] = book
if DEBUG:
self.log.info(" found calibre orphan '%s' in Library|Books" % book.Name)
self.log.info(" found iTunes PTF '%s' in Library|Books" % book.Name)
library_books[path] = book
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString))
self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString))
except:
if DEBUG:
self.log.info(" no books in library")
# finally:
# pythoncom.CoUninitialize()
self.library_orphans = library_orphans
return library_books
@ -1905,44 +1794,36 @@ class ITUNES(DevicePlugin):
self.version[0],self.version[1],self.version[2]))
self.log.info(" iTunes_media: %s" % self.iTunes_media)
def _purge_orphans(self,cached_books):
def _purge_orphans(self,library_books, cached_books):
'''
Scan self.library_orphans for any paths not on device
Remove any true orphans from iTunes
This occurs when recipes are uploaded in a previous session
and the book has since been deleted on the device
Scan library_books for any paths not on device
Remove any iTunes orphans originally added by calibre
This occurs when the user deletes a book in iBooks while disconnected
'''
if DEBUG:
self.log.info(" ITUNES._purge_orphans")
self.log.info("\n ITUNES._purge_orphans")
#self._dump_library_books(library_books)
#self.log.info(" cached_books:\n %s" % "\n ".join(cached_books.keys()))
orphan_paths = {}
if isosx:
for orphan in self.library_orphans:
path = self.path_template % (self.library_orphans[orphan].name(),
self.library_orphans[orphan].artist())
orphan_paths[path] = self.library_orphans[orphan]
# Scan orphan_paths for paths not found in cached_books
for orphan in orphan_paths.keys():
if orphan not in cached_books:
for book in library_books:
if isosx:
if book not in cached_books and \
str(library_books[book].description()).startswith(self.description_prefix):
if DEBUG:
self.log.info(" '%s' not found on device, removing from iTunes" % orphan)
self.iTunes.delete(orphan_paths[orphan])
elif iswindows:
for orphan in self.library_orphans:
path = self.path_template % (self.library_orphans[orphan].Name,
self.library_orphans[orphan].Artist)
orphan_paths[path] = self.library_orphans[orphan]
# Scan orphan_paths for paths not found in cached_books
for orphan in orphan_paths.keys():
if orphan not in cached_books:
self.log.info(" '%s' not found on iDevice, removing from iTunes" % book)
btr = { 'title':library_books[book].name(),
'author':library_books[book].artist(),
'lib_book':library_books[book]}
self._remove_from_iTunes(btr)
elif iswindows:
if book not in cached_books and \
library_books[book].Description.startswith(self.description_prefix):
if DEBUG:
self.log.info(" '%s' not found on device, removing from iTunes" % orphan)
orphan_paths[orphan].Delete()
self.log.info(" '%s' not found on iDevice, removing from iTunes" % book)
btr = { 'title':library_books[book].Name,
'author':library_books[book].Artist,
'lib_book':library_books[book]}
self._remove_from_iTunes(btr)
def _remove_existing_copies(self,path,file,metadata):
'''
@ -2040,7 +1921,7 @@ class ITUNES(DevicePlugin):
except:
# We get here if there was an error with .location().path
self.log.info(" removing orphan '%s' from iTunes" % cached_book['title'])
self.log.info(" removing orphan '%s' from iTunes" % cached_book['title'])
self.iTunes.delete(cached_book['lib_book'])
@ -2049,33 +1930,33 @@ class ITUNES(DevicePlugin):
Assume we're wrapped in a pythoncom
Windows stores the book under a common author directory, so we just delete the .epub
'''
book = self._find_library_book(cached_book)
if book:
try:
book = cached_book['lib_book']
path = book.Location
except:
book = self._find_library_book(cached_book)
path = book.Location
storage_path = os.path.split(book.Location)
if book.Location.startswith(self.iTunes_media):
if DEBUG:
self.log.info(" removing '%s' at %s" %
(cached_book['title'], path))
try:
os.remove(path)
except:
self.log.warning(" could not find '%s' in iTunes storage" % path)
try:
os.rmdir(storage_path[0])
self.log.info(" removed folder '%s'" % storage_path[0])
except:
self.log.info(" folder '%s' not found or not empty" % storage_path[0])
# Delete from iTunes database
else:
self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title'])
book.Delete()
storage_path = os.path.split(book.Location)
if book.Location.startswith(self.iTunes_media):
if DEBUG:
self.log.info(" removing '%s' at %s" %
(cached_book['title'], path))
try:
os.remove(path)
except:
self.log.warning(" could not find '%s' in iTunes storage" % path)
try:
os.rmdir(storage_path[0])
self.log.info(" removed folder '%s'" % storage_path[0])
except:
self.log.info(" folder '%s' not found or not empty" % storage_path[0])
# Delete from iTunes database
else:
self.log.warning(" could not find '%s' in iTunes database" % cached_book['title'])
self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title'])
book.Delete()
def _update_device(self, msg='', wait=True):
'''

View File

@ -103,8 +103,8 @@ class CoverManager(object):
32)]
img_data = create_cover_page(lines, I('library.png'))
id, href = self.oeb.manifest.generate('cover_image',
'cover_image.png')
item = self.oeb.manifest.add(id, href, guess_type('t.png')[0],
'cover_image.jpg')
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
data=img_data)
m.clear('cover')
m.add('cover', item.id)

1200
src/calibre/gui2/actions.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -83,7 +83,6 @@ if pictureflow is not None:
self.setFocusPolicy(Qt.WheelFocus)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.setZoomFactor(150)
def sizeHint(self):
return self.minimumSize()

View File

@ -3,24 +3,26 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{
import os, traceback, Queue, time, socket, cStringIO, re
import os, traceback, Queue, time, socket, cStringIO, re, sys
from threading import Thread, RLock
from itertools import repeat
from functools import partial
from binascii import unhexlify
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
Qt, pyqtSignal
Qt, pyqtSignal, QColor, QPainter
from PyQt4.QtSvg import QSvgRenderer
from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins
from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import UserFeedback
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \
question_dialog
pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
from calibre import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename
@ -597,10 +599,204 @@ class Emailer(Thread): # {{{
# }}}
class DeviceMixin(object):
class DeviceMixin(object): # {{{
def __init__(self):
self.db_book_uuid_cache = set()
self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
self.device_connected = None
self.emailer = Emailer()
self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
self.job_manager, Dispatcher(self.status_bar.show_message))
self.device_manager.start()
def set_default_thumbnail(self, height):
r = QSvgRenderer(I('book.svg'))
pixmap = QPixmap(height, height)
pixmap.fill(QColor(255,255,255))
p = QPainter(pixmap)
r.render(p)
p.end()
self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap))
def connect_to_folder(self):
dir = choose_dir(self, 'Select Device Folder',
_('Select folder to open as device'))
if dir is not None:
self.device_manager.connect_to_folder(dir)
def disconnect_from_folder(self):
self.device_manager.disconnect_folder()
def _sync_action_triggered(self, *args):
m = getattr(self, '_sync_menu', None)
if m is not None:
m.trigger_default()
def create_device_menu(self):
self._sync_menu = DeviceMenu(self)
self.action_sync.setMenu(self._sync_menu)
self.connect(self._sync_menu,
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder)
if self.device_connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
if self.device_connected == 'folder':
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
else:
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
def device_job_exception(self, job):
'''
Handle exceptions in threaded device jobs.
'''
if isinstance(getattr(job, 'exception', None), UserFeedback):
ex = job.exception
func = {UserFeedback.ERROR:error_dialog,
UserFeedback.WARNING:warning_dialog,
UserFeedback.INFO:info_dialog}[ex.level]
return func(self, _('Failed'), ex.msg, det_msg=ex.details if
ex.details else '', show=True)
try:
if 'Could not read 32 bytes on the control bus.' in \
unicode(job.details):
error_dialog(self, _('Error talking to device'),
_('There was a temporary error talking to the '
'device. Please unplug and reconnect the device '
'and or reboot.')).show()
return
except:
pass
try:
prints(job.details, file=sys.stderr)
except:
pass
if not self.device_error_dialog.isVisible():
self.device_error_dialog.setDetailedText(job.details)
self.device_error_dialog.show()
# Device connected {{{
def device_detected(self, connected, is_folder_device):
'''
Called when a device is connected to the computer.
'''
if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
if is_folder_device:
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
self.device_manager.get_device_information(\
Dispatcher(self.info_read))
self.set_default_thumbnail(\
self.device_manager.device.THUMBNAIL_HEIGHT)
self.status_bar.show_message(_('Device: ')+\
self.device_manager.device.__class__.get_gui_name()+\
_(' detected.'), 3000)
self.device_connected = 'device' if not is_folder_device else 'folder'
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.location_view.model().device_connected(self.device_manager.device)
self.eject_action.setEnabled(True)
self.refresh_ondevice_info (device_connected = True, reset_only = True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
self.device_connected = None
self._sync_menu.enable_device_actions(False)
self.location_view.model().update_devices()
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=' '))
self.device_info = ' '
if self.current_view() != self.library_view:
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.eject_action.setEnabled(False)
self.refresh_ondevice_info (device_connected = False)
def info_read(self, job):
'''
Called once device information has been read.
'''
if job.failed:
return self.device_job_exception(job)
info, cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.device_info = _('Connected ')+info[0]
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=self.device_info))
self.device_manager.books(Dispatcher(self.metadata_downloaded))
def metadata_downloaded(self, job):
'''
Called once metadata has been read for all books on the device.
'''
if job.failed:
self.device_job_exception(job)
return
self.set_books_in_library(job.result, reset=True)
mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist)
self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_a_view.set_database(cardalist)
self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_b_view.set_database(cardblist)
self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.sync_news()
self.sync_catalogs()
self.refresh_ondevice_info(device_connected = True)
def refresh_ondevice_info(self, device_connected, reset_only = False):
'''
Force the library view to refresh, taking into consideration
books information
'''
self.book_on_device(None, reset=True)
if reset_only:
return
self.library_view.set_device_connected(device_connected)
# }}}
def remove_paths(self, paths):
return self.device_manager.delete_books(
Dispatcher(self.books_deleted), paths)
def books_deleted(self, job):
'''
Called once deletion is done on the device
'''
for view in (self.memory_view, self.card_a_view, self.card_b_view):
view.model().deletion_done(job, job.failed)
if job.failed:
self.device_job_exception(job)
return
if self.delete_memory.has_key(job):
paths, model = self.delete_memory.pop(job)
self.device_manager.remove_books_from_metadata(paths,
self.booklists())
model.paths_deleted(paths)
self.upload_booklists()
# Clear the ondevice info so it will be recomputed
self.book_on_device(None, None, reset=True)
# We want to reset all the ondevice flags in the library. Use a big
# hammer, so we don't need to worry about whether some succeeded or not
self.library_view.model().refresh()
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
@ -1220,3 +1416,6 @@ class DeviceMixin(object):
# Correct the metadata cache on device.
if self.device_manager.is_device_connected:
self.device_manager.sync_booklists(None, booklists)
# }}}

View File

@ -47,7 +47,7 @@ class ToolbarMixin(object): # {{{
def __init__(self):
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False))
partial(self.edit_metadata, False, bulk=False))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))

View File

@ -85,7 +85,9 @@ typedef long PFreal;
typedef unsigned short QRgb565;
#define FONT_SIZE 18
#define REFLECTION_FACTOR 1.5
#define MAX(x, y) ((x > y) ? x : y)
#define RGB565_RED_MASK 0xF800
#define RGB565_GREEN_MASK 0x07E0
@ -124,6 +126,7 @@ inline PFreal floatToFixed(float val)
return (PFreal)(val*PFREAL_ONE);
}
// sinTable {{{
#define IANGLE_MAX 1024
#define IANGLE_MASK 1023
@ -293,6 +296,7 @@ int main(int, char**)
return 0;
}
#endif
// }}}
inline PFreal fsin(int iangle)
{
@ -315,6 +319,8 @@ struct SlideInfo
PFreal cy;
};
// PicturePlowPrivate {{{
class PictureFlowPrivate
{
public:
@ -369,6 +375,7 @@ private:
int slideWidth;
int slideHeight;
int fontSize;
int zoom;
int queueLength;
@ -406,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
slideWidth = 200;
slideHeight = 200;
fontSize = 10;
zoom = 100;
centerIndex = 0;
@ -542,8 +550,11 @@ void PictureFlowPrivate::showSlide(int index)
void PictureFlowPrivate::resize(int w, int h)
{
slideHeight = int(float(h)/2.);
if (w < 10) w = 10;
if (h < 10) h = 10;
slideHeight = int(float(h)/REFLECTION_FACTOR);
slideWidth = int(float(slideHeight) * 2/3.);
fontSize = MAX(int(h/20.), 12);
recalc(w, h);
resetSlides();
triggerRender();
@ -592,8 +603,8 @@ static QImage prepareSurface(QImage img, int w, int h)
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
// slightly larger, to accomodate for the reflection
int hs = h * 2;
int hofs = h / 3;
int hs = int(h * REFLECTION_FACTOR);
int hofs = 0;
// offscreen buffer: black is sweet
QImage result(hs, w, QImage::Format_RGB16);
@ -715,13 +726,13 @@ void PictureFlowPrivate::render()
QFont font = QFont();
font.setBold(true);
font.setPointSize(FONT_SIZE);
font.setPixelSize(fontSize);
painter.setFont(font);
painter.setPen(Qt::white);
//painter.setPen(QColor(255,255,255,127));
if (centerIndex < slideCount() && centerIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-FONT_SIZE*3),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3),
Qt::AlignCenter, slideImages->caption(centerIndex));
painter.end();
@ -766,7 +777,7 @@ void PictureFlowPrivate::render()
QFont font = QFont();
font.setBold(true);
font.setPointSize(FONT_SIZE);
font.setPixelSize(fontSize);
painter.setFont(font);
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
@ -774,12 +785,12 @@ void PictureFlowPrivate::render()
painter.setPen(QColor(255,255,255, (255-fade) ));
if (leftTextIndex < sc && leftTextIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
Qt::AlignCenter, slideImages->caption(leftTextIndex));
painter.setPen(QColor(255,255,255, fade));
if (leftTextIndex+1 < sc && leftTextIndex > -2)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3),
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
@ -893,7 +904,7 @@ int col1, int col2)
int center = (sh*BILINEAR_STRETCH_VER/2);
int dy = dist*BILINEAR_STRETCH_VER / h;
#else
int center = (sh/2);
int center = sh/2;
int dy = dist / h;
#endif
int p1 = center*PFREAL_ONE - dy/2;
@ -1110,8 +1121,9 @@ void PictureFlowPrivate::clearSurfaceCache()
surfaceCache.clear();
}
// -----------------------------------------
// }}}
// PictureFlow {{{
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
{
d = new PictureFlowPrivate(this, queueLength);
@ -1387,3 +1399,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
int FlowImages::count() { return 0; }
QImage FlowImages::image(int index) { index=0; return QImage(); }
QString FlowImages::caption(int index) {index=0; return QString(); }
// }}}

View File

@ -2,9 +2,10 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, collections
from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea
from PyQt4.QtCore import Qt, QSize, pyqtSignal
from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
QPropertyAnimation, QEasingCurve
from calibre import fit_image, preferred_encoding, isosx
from calibre.gui2 import config
@ -50,6 +51,10 @@ class BookInfoDisplay(QWidget):
def __init__(self, coverpath=I('book.svg')):
QLabel.__init__(self)
self.animation = QPropertyAnimation(self, 'size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.setMaximumWidth(81)
self.setMaximumHeight(108)
self.default_pixmap = QPixmap(coverpath)
@ -58,6 +63,7 @@ class BookInfoDisplay(QWidget):
self.setPixmap(self.default_pixmap)
def do_layout(self):
self.animation.stop()
pixmap = self.pixmap()
pwidth, pheight = pixmap.width(), pixmap.height()
width, height = fit_image(pwidth, pheight,
@ -68,11 +74,12 @@ class BookInfoDisplay(QWidget):
except ZeroDivisionError:
aspect_ratio = 1
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
self.animation.setEndValue(self.maximumSize())
def setPixmap(self, pixmap):
QLabel.setPixmap(self, pixmap)
self.do_layout()
self.animation.start()
def sizeHint(self):
return QSize(self.maximumWidth(), self.maximumHeight())

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import traceback
from PyQt4.QtCore import QThread, pyqtSignal
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
import mechanize
from calibre.constants import __version__, iswindows, isosx
from calibre.constants import __appname__, __version__, iswindows, isosx
from calibre import browser
from calibre.utils.config import prefs
from calibre.gui2 import config, dynamic, question_dialog
URL = 'http://status.calibre-ebook.com/latest'
@ -36,3 +37,35 @@ class CheckForUpdates(QThread):
traceback.print_exc()
self.sleep(self.INTERVAL)
class UpdateMixin(object):
def __init__(self, opts):
if not opts.no_update_check:
self.update_checker = CheckForUpdates(self)
self.update_checker.update_found.connect(self.update_found,
type=Qt.QueuedConnection)
self.update_checker.start()
def update_found(self, version):
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
url = 'http://calibre-ebook.com/download_%s'%os
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
'Latest version: <a href="%s">%s</a></span>')%(url, version)
self.vanity.setText(self.vanity_template%\
(dict(version=self.latest_version,
device=self.device_info)))
self.vanity.update()
if config.get('new_version_notification') and \
dynamic.get('update to version %s'%version, True):
if question_dialog(self, _('Update available'),
_('%s has been updated to version %s. '
'See the <a href="http://calibre-ebook.com/whats-new'
'">new features</a>. Visit the download pa'
'ge?')%(__appname__, version)):
url = 'http://calibre-ebook.com/download_'+\
('windows' if iswindows else 'osx' if isosx else 'linux')
QDesktopServices.openUrl(QUrl(url))
dynamic.set('update to version %s'%version, False)

View File

@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools
from itertools import repeat
from datetime import timedelta
from PyQt4.QtCore import QThread, QReadWriteLock
from PyQt4.QtGui import QImage
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import title_sort
from calibre import fit_image
class CoverCache(QThread):
@ -96,6 +96,11 @@ class CoverCache(QThread):
img.loadFromData(data)
if img.isNull():
continue
scaled, nwidth, nheight = fit_image(img.width(),
img.height(), 600, 800)
if scaled:
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
Qt.SmoothTransformation)
except:
continue
self.cache_lock.lockForWrite()

View File

@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML
.. automember:: BasicNewsRecipe.remove_javascript
.. automethod:: BasicNewsRecipe.prepreprocess_html
.. automethod:: BasicNewsRecipe.skip_ad_pages
.. automethod:: BasicNewsRecipe.preprocess_html

View File

@ -175,7 +175,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
p.DestroyMagickWand(canvas)
def create_cover_page(top_lines, logo_path, width=590, height=750,
bgcolor='white', output_format='png'):
bgcolor='white', output_format='jpg'):
ans = None
with p.ImageMagick():
canvas = create_canvas(width, height, bgcolor)

View File

@ -413,18 +413,19 @@ class BasicNewsRecipe(Recipe):
return url
return article.get('link', None)
def prepreprocess_html(self, soup):
def skip_ad_pages(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before
any of the cleanup attributes like remove_tags, keep_only_tags are
applied. Note that preprocess_regexps will have already been applied.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.
It is meant to allow the recipe to skip ad pages. If the soup represents
an ad page, return the HTML of the real page. Otherwise return
None.
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
instance containing the downloaded :term:`HTML`.
'''
return soup
return None
def preprocess_html(self, soup):
@ -628,7 +629,7 @@ class BasicNewsRecipe(Recipe):
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
'prepreprocess_html', 'preprocess_html', 'remove_tags_after',
'skip_ad_pages', 'preprocess_html', 'remove_tags_after',
'remove_tags_before', 'is_link_wanted'):
setattr(self.web2disk_options, extra, getattr(self, extra))
self.web2disk_options.postprocess_html = self._postprocess_html
@ -801,11 +802,6 @@ class BasicNewsRecipe(Recipe):
.calibre_navbar {
font-family:monospace;
}
hr {
border-color:gray;
border-style:solid;
border-width:thin;
}
'''

View File

@ -108,7 +108,7 @@ class TouchscreenNavBarTemplate(Template):
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100',
style='text-align:'+align))
if bottom:
navbar.append(HR())
navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white"))
text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
p[0].tail = ' from '
@ -136,7 +136,7 @@ class TouchscreenNavBarTemplate(Template):
navbar.iterchildren(reversed=True).next().tail = ' | '
if not bottom:
navbar.append(HR())
navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white"))
self.root = HTML(head, BODY(navbar))
@ -193,6 +193,8 @@ class TouchscreenIndexTemplate(Template):
div = DIV(
masthead_p,
PT(date, style='text-align:center'),
#DIV(style="border-color:gray;border-top-style:solid;border-width:thin"),
DIV(style="border-top:1px solid gray;border-bottom:1em solid white"),
toc)
self.root = HTML(head, BODY(div))
@ -256,10 +258,9 @@ class TouchscreenFeedTemplate(Template):
head.append(STYLE(extra_css, type='text/css'))
body = BODY(style='page-break-before:always')
div = DIV(
H2(feed.title,
CLASS('calibre_feed_title', 'calibre_rescale_160')),
CLASS('calibre_rescale_100')
)
H2(feed.title, CLASS('calibre_feed_title', 'calibre_rescale_160')),
DIV(style="border-top:1px solid gray;border-bottom:1em solid white")
)
body.append(div)
if getattr(feed, 'image', None):
div.append(DIV(IMG(
@ -278,17 +279,33 @@ class TouchscreenFeedTemplate(Template):
if not getattr(article, 'downloaded', False):
continue
tr = TR()
td = TD(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url))
)
if article.author:
td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(td)
if True:
div_td = DIV(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url)),
style="display:inline-block")
if article.author:
div_td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
div_td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(TD(div_td))
else:
td = TD(
A(article.title, CLASS('summary_headline','calibre_rescale_120',
href=article.url))
)
if article.author:
td.append(DIV(article.author,
CLASS('summary_byline', 'calibre_rescale_100')))
if article.summary:
td.append(DIV(cutoff(article.text_summary),
CLASS('summary_text', 'calibre_rescale_100')))
tr.append(td)
toc.append(tr)
div.append(toc)

View File

@ -136,7 +136,7 @@ class RecursiveFetcher(object):
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup)
self.prepreprocess_html_ext = getattr(options, 'skip_ad_pages', lambda soup: None)
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
self._is_link_wanted = getattr(options, 'is_link_wanted',
default_is_link_wanted)
@ -154,7 +154,9 @@ class RecursiveFetcher(object):
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
soup = self.prepreprocess_html_ext(soup)
replace = self.prepreprocess_html_ext(soup)
if replace is not None:
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
if self.keep_only_tags:
body = Tag(soup, 'body')