mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Cover grid: Add a disk cache for rendered thumbnails
This commit is contained in:
parent
437746f139
commit
9859812ec9
@ -1225,6 +1225,22 @@ class DB(object):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def cover_or_cache(self, path, timestamp):
|
||||||
|
path = os.path.abspath(os.path.join(self.library_path, path, 'cover.jpg'))
|
||||||
|
try:
|
||||||
|
stat = os.stat(path)
|
||||||
|
except EnvironmentError:
|
||||||
|
return False, None, None
|
||||||
|
if abs(timestamp - stat.st_mtime) < 0.1:
|
||||||
|
return True, None, None
|
||||||
|
try:
|
||||||
|
f = lopen(path, 'rb')
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
f = lopen(path, 'rb')
|
||||||
|
with f:
|
||||||
|
return True, f.read(), stat.st_mtime
|
||||||
|
|
||||||
def set_cover(self, book_id, path, data):
|
def set_cover(self, book_id, path, data):
|
||||||
path = os.path.abspath(os.path.join(self.library_path, path))
|
path = os.path.abspath(os.path.join(self.library_path, path))
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
|
@ -587,6 +587,14 @@ class Cache(object):
|
|||||||
ret = i
|
ret = i
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@read_api
|
||||||
|
def cover_or_cache(self, book_id, timestamp):
|
||||||
|
try:
|
||||||
|
path = self._field_for('path', book_id).replace('/', os.sep)
|
||||||
|
except AttributeError:
|
||||||
|
return False, None, None
|
||||||
|
return self.backend.cover_or_cache(path, timestamp)
|
||||||
|
|
||||||
@read_api
|
@read_api
|
||||||
def cover_last_modified(self, book_id):
|
def cover_last_modified(self, book_id):
|
||||||
try:
|
try:
|
||||||
@ -1032,8 +1040,8 @@ class Cache(object):
|
|||||||
path = self._field_for('path', book_id).replace('/', os.sep)
|
path = self._field_for('path', book_id).replace('/', os.sep)
|
||||||
|
|
||||||
self.backend.set_cover(book_id, path, data)
|
self.backend.set_cover(book_id, path, data)
|
||||||
for cc in self.cover_caches:
|
for cc in self.cover_caches:
|
||||||
cc.invalidate(book_id)
|
cc.invalidate(book_id_data_map)
|
||||||
return self._set_field('cover', {
|
return self._set_field('cover', {
|
||||||
book_id:(0 if data is None else 1) for book_id, data in book_id_data_map.iteritems()})
|
book_id:(0 if data is None else 1) for book_id, data in book_id_data_map.iteritems()})
|
||||||
|
|
||||||
@ -1352,8 +1360,7 @@ class Cache(object):
|
|||||||
self._search_api.discard_books(book_ids)
|
self._search_api.discard_books(book_ids)
|
||||||
self._clear_caches(book_ids=book_ids, template_cache=False, search_cache=False)
|
self._clear_caches(book_ids=book_ids, template_cache=False, search_cache=False)
|
||||||
for cc in self.cover_caches:
|
for cc in self.cover_caches:
|
||||||
for book_id in book_ids:
|
cc.invalidate(book_ids)
|
||||||
cc.invalidate(book_id)
|
|
||||||
|
|
||||||
@read_api
|
@read_api
|
||||||
def author_sort_strings_for_books(self, book_ids):
|
def author_sort_strings_for_books(self, book_ids):
|
||||||
|
83
src/calibre/db/tests/utils.py
Normal file
83
src/calibre/db/tests/utils.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from calibre import walk
|
||||||
|
from calibre.db.tests.base import BaseTest
|
||||||
|
from calibre.db.utils import ThumbnailCache
|
||||||
|
|
||||||
|
class UtilsTest(BaseTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.tdir = self.mkdtemp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tdir)
|
||||||
|
|
||||||
|
def init_tc(self, name='1', max_size=1):
|
||||||
|
return ThumbnailCache(name=name, location=self.tdir, max_size=max_size, test_mode=True)
|
||||||
|
|
||||||
|
def basic_fill(self, c, num=5):
|
||||||
|
total = 0
|
||||||
|
for i in range(1, num+1):
|
||||||
|
sz = i * 1000
|
||||||
|
c.insert(i, i, (('%d'%i) * sz).encode('ascii'))
|
||||||
|
total += sz
|
||||||
|
return total
|
||||||
|
|
||||||
|
def test_thumbnail_cache(self): # {{{
|
||||||
|
' Test the operation of the thumbnail cache '
|
||||||
|
c = self.init_tc()
|
||||||
|
self.assertFalse(hasattr(c, 'total_size'), 'index read on initialization')
|
||||||
|
c.invalidate(666)
|
||||||
|
self.assertFalse(hasattr(c, 'total_size'), 'index read on invalidate')
|
||||||
|
|
||||||
|
self.assertEqual(self.basic_fill(c), c.total_size)
|
||||||
|
self.assertEqual(5, len(c))
|
||||||
|
|
||||||
|
for i in (3, 4, 2, 5, 1):
|
||||||
|
data, ts = c[i]
|
||||||
|
self.assertEqual(i, ts, 'timestamp not correct')
|
||||||
|
self.assertEqual((('%d'%i) * (i*1000)).encode('ascii'), data)
|
||||||
|
c.set_group_id('a')
|
||||||
|
self.basic_fill(c)
|
||||||
|
order = tuple(c.items)
|
||||||
|
ts = c.current_size
|
||||||
|
c.shutdown()
|
||||||
|
c = self.init_tc()
|
||||||
|
self.assertEqual(c.current_size, ts, 'size not preserved after restart')
|
||||||
|
self.assertEqual(order, tuple(c.items), 'order not preserved after restart')
|
||||||
|
c.shutdown()
|
||||||
|
c = self.init_tc()
|
||||||
|
c.invalidate((1,))
|
||||||
|
self.assertIsNone(c[1][1], 'invalidate before load_index() failed')
|
||||||
|
c.invalidate((2,))
|
||||||
|
self.assertIsNone(c[2][1], 'invalidate after load_index() failed')
|
||||||
|
c.set_group_id('a')
|
||||||
|
c[1]
|
||||||
|
c.set_size(0.001)
|
||||||
|
self.assertLessEqual(c.current_size, 1024, 'set_size() failed')
|
||||||
|
self.assertEqual(len(c), 1)
|
||||||
|
self.assertIn(1, c)
|
||||||
|
c.insert(9, 9, b'x' * (c.max_size-1))
|
||||||
|
self.assertEqual(len(c), 1)
|
||||||
|
self.assertLessEqual(c.current_size, c.max_size, 'insert() did not prune')
|
||||||
|
self.assertIn(9, c)
|
||||||
|
c.empty()
|
||||||
|
self.assertEqual(c.total_size, 0)
|
||||||
|
self.assertEqual(len(c), 0)
|
||||||
|
self.assertEqual(tuple(walk(c.location)), ())
|
||||||
|
c = self.init_tc()
|
||||||
|
self.basic_fill(c)
|
||||||
|
self.assertEqual(len(c), 5)
|
||||||
|
c.set_thumbnail_size(200, 201)
|
||||||
|
self.assertIsNone(c[1][0])
|
||||||
|
self.assertEqual(len(c), 0)
|
||||||
|
self.assertEqual(tuple(walk(c.location)), ())
|
||||||
|
# }}}
|
297
src/calibre/db/utils.py
Normal file
297
src/calibre/db/utils.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os, errno, cPickle, sys
|
||||||
|
from collections import OrderedDict, namedtuple
|
||||||
|
from future_builtins import map
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
from calibre import as_unicode, prints
|
||||||
|
from calibre.constants import cache_dir
|
||||||
|
|
||||||
|
Entry = namedtuple('Entry', 'path size timestamp thumbnail_size')
|
||||||
|
class CacheError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ThumbnailCache(object):
|
||||||
|
|
||||||
|
' This is a persistent disk cache to speed up loading and resizing of covers '
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
max_size=1024, # The maximum disk space in MB
|
||||||
|
name='thumbnail-cache', # The name of this cache (should be unique in location)
|
||||||
|
thumbnail_size=(100, 100), # The size of the thumbnails, can be changed
|
||||||
|
location=None, # The location for this cache, if None cache_dir() is used
|
||||||
|
test_mode=False, # Used for testing
|
||||||
|
min_disk_cache=0): # If the size is set less than or equal to this value, the cache is disabled.
|
||||||
|
self.location = os.path.join(location or cache_dir(), name)
|
||||||
|
if max_size <= min_disk_cache:
|
||||||
|
max_size = 0
|
||||||
|
self.max_size = int(max_size * (1024**2))
|
||||||
|
self.group_id = 'group'
|
||||||
|
self.thumbnail_size = thumbnail_size
|
||||||
|
self.size_changed = False
|
||||||
|
self.lock = Lock()
|
||||||
|
self.min_disk_cache = min_disk_cache
|
||||||
|
if test_mode:
|
||||||
|
self.log = self.fail_on_error
|
||||||
|
|
||||||
|
def log(self, *args, **kwargs):
|
||||||
|
kwargs['file'] = sys.stderr
|
||||||
|
prints(*args, **kwargs)
|
||||||
|
|
||||||
|
def fail_on_error(self, *args, **kwargs):
|
||||||
|
msg = ' '.join(args)
|
||||||
|
raise CacheError(msg)
|
||||||
|
|
||||||
|
def _do_delete(self, path):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to delete cached thumbnail file:', as_unicode(err))
|
||||||
|
|
||||||
|
def _load_index(self):
|
||||||
|
'Load the index, automatically removing incorrectly sized thumbnails and pruning to fit max_size'
|
||||||
|
try:
|
||||||
|
os.makedirs(self.location)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != errno.EEXIST:
|
||||||
|
self.log('Failed to make thumbnail cache dir:', as_unicode(err))
|
||||||
|
self.total_size = 0
|
||||||
|
self.items = OrderedDict()
|
||||||
|
order = self._read_order()
|
||||||
|
def listdir(*args):
|
||||||
|
try:
|
||||||
|
return os.listdir(os.path.join(*args))
|
||||||
|
except EnvironmentError:
|
||||||
|
return () # not a directory or no permission or whatever
|
||||||
|
entries = ('/'.join((parent, subdir, entry))
|
||||||
|
for parent in listdir(self.location)
|
||||||
|
for subdir in listdir(self.location, parent)
|
||||||
|
for entry in listdir(self.location, parent, subdir))
|
||||||
|
|
||||||
|
invalidate = set()
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.location, 'invalidate'), 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
except EnvironmentError as err:
|
||||||
|
if getattr(err, 'errno', None) != errno.ENOENT:
|
||||||
|
self.log('Failed to read thumbnail invalidate data:', as_unicode(err))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(self.location, 'invalidate'))
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to remove thumbnail invalidate data:', as_unicode(err))
|
||||||
|
else:
|
||||||
|
def record(line):
|
||||||
|
try:
|
||||||
|
uuid, book_id = line.partition(' ')[0::2]
|
||||||
|
book_id = int(book_id)
|
||||||
|
return (uuid, book_id)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
invalidate = {record(x) for x in raw.splitlines()}
|
||||||
|
items = []
|
||||||
|
try:
|
||||||
|
for entry in entries:
|
||||||
|
try:
|
||||||
|
uuid, name = entry.split('/')[0::2]
|
||||||
|
book_id, timestamp, size, thumbnail_size = name.split('-')
|
||||||
|
book_id, timestamp, size = int(book_id), float(timestamp), int(size)
|
||||||
|
thumbnail_size = tuple(map(int, thumbnail_size.partition('x')[0::2]))
|
||||||
|
except (ValueError, TypeError, IndexError, KeyError, AttributeError):
|
||||||
|
continue
|
||||||
|
key = (uuid, book_id)
|
||||||
|
path = os.path.join(self.location, entry)
|
||||||
|
if self.thumbnail_size == thumbnail_size and key not in invalidate:
|
||||||
|
items.append((key, Entry(path, size, timestamp, thumbnail_size)))
|
||||||
|
self.total_size += size
|
||||||
|
else:
|
||||||
|
self._do_delete(path)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to read thumbnail cache dir:', as_unicode(err))
|
||||||
|
|
||||||
|
self.items = OrderedDict(sorted(items, key=lambda x:order.get(hash(x[0]), 0)))
|
||||||
|
self._apply_size()
|
||||||
|
|
||||||
|
def _invalidate_sizes(self):
|
||||||
|
if self.size_changed:
|
||||||
|
size = self.thumbnail_size
|
||||||
|
remove = (key for key, entry in self.items.iteritems() if size != entry.thumbnail_size)
|
||||||
|
for key in remove:
|
||||||
|
self._remove(key)
|
||||||
|
self.size_changed = False
|
||||||
|
|
||||||
|
def _remove(self, key):
|
||||||
|
entry = self.items.pop(key, None)
|
||||||
|
if entry is not None:
|
||||||
|
self._do_delete(entry.path)
|
||||||
|
self.total_size -= entry.size
|
||||||
|
|
||||||
|
def _apply_size(self):
|
||||||
|
while self.total_size > self.max_size and self.items:
|
||||||
|
entry = self.items.popitem(last=False)[1]
|
||||||
|
self._do_delete(entry.path)
|
||||||
|
self.total_size -= entry.size
|
||||||
|
|
||||||
|
def _write_order(self):
|
||||||
|
if hasattr(self, 'items'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.location, 'order'), 'wb') as f:
|
||||||
|
f.write(cPickle.dumps(tuple(map(hash, self.items)), -1))
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to save thumbnail cache order:', as_unicode(err))
|
||||||
|
|
||||||
|
def _read_order(self):
|
||||||
|
order = {}
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.location, 'order'), 'rb') as f:
|
||||||
|
order = cPickle.loads(f.read())
|
||||||
|
order = {k:i for i, k in enumerate(order)}
|
||||||
|
except Exception as err:
|
||||||
|
if getattr(err, 'errno', None) != errno.ENOENT:
|
||||||
|
self.log('Failed to load thumbnail cache order:', as_unicode(err))
|
||||||
|
return order
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
with self.lock:
|
||||||
|
self._write_order()
|
||||||
|
|
||||||
|
def set_group_id(self, group_id):
|
||||||
|
with self.lock:
|
||||||
|
self.group_id = group_id
|
||||||
|
|
||||||
|
def set_thumbnail_size(self, width, height):
|
||||||
|
with self.lock:
|
||||||
|
self.thumbnail_size = (width, height)
|
||||||
|
self.size_changed = True
|
||||||
|
|
||||||
|
def insert(self, book_id, timestamp, data):
|
||||||
|
if self.max_size < len(data):
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if not hasattr(self, 'total_size'):
|
||||||
|
self._load_index()
|
||||||
|
self._invalidate_sizes()
|
||||||
|
ts = ('%.2f' % timestamp).replace('.00', '')
|
||||||
|
path = '%s%s%s%s%d-%s-%d-%dx%d' % (
|
||||||
|
self.group_id, os.sep, book_id % 100, os.sep,
|
||||||
|
book_id, ts, len(data), self.thumbnail_size[0], self.thumbnail_size[1])
|
||||||
|
path = os.path.join(self.location, path)
|
||||||
|
key = (self.group_id, book_id)
|
||||||
|
e = self.items.pop(key, None)
|
||||||
|
self.total_size -= getattr(e, 'size', 0)
|
||||||
|
try:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
d = os.path.dirname(path)
|
||||||
|
if not os.path.exists(d):
|
||||||
|
try:
|
||||||
|
os.makedirs(d)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to write cached thumbnail:', path, as_unicode(err))
|
||||||
|
return self._apply_size()
|
||||||
|
else:
|
||||||
|
self.log('Failed to write cached thumbnail:', path, as_unicode(err))
|
||||||
|
return self._apply_size()
|
||||||
|
self.items[key] = Entry(path, len(data), timestamp, self.thumbnail_size)
|
||||||
|
self.total_size += len(data)
|
||||||
|
self._apply_size()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
with self.lock:
|
||||||
|
try:
|
||||||
|
return len(self.items)
|
||||||
|
except AttributeError:
|
||||||
|
self._load_index()
|
||||||
|
return len(self.items)
|
||||||
|
|
||||||
|
def __contains__(self, book_id):
|
||||||
|
with self.lock:
|
||||||
|
try:
|
||||||
|
return (self.group_id, book_id) in self.items
|
||||||
|
except AttributeError:
|
||||||
|
self._load_index()
|
||||||
|
return (self.group_id, book_id) in self.items
|
||||||
|
|
||||||
|
def __getitem__(self, book_id):
|
||||||
|
with self.lock:
|
||||||
|
if not hasattr(self, 'total_size'):
|
||||||
|
self._load_index()
|
||||||
|
self._invalidate_sizes()
|
||||||
|
key = (self.group_id, book_id)
|
||||||
|
entry = self.items.pop(key, None)
|
||||||
|
if entry is None:
|
||||||
|
return None, None
|
||||||
|
if entry.thumbnail_size != self.thumbnail_size:
|
||||||
|
try:
|
||||||
|
os.remove(entry.path)
|
||||||
|
except EnvironmentError as err:
|
||||||
|
if getattr(err, 'errno', None) != errno.ENOENT:
|
||||||
|
self.log('Failed to remove cached thumbnail:', entry.path, as_unicode(err))
|
||||||
|
self.total_size -= entry.size
|
||||||
|
return None, None
|
||||||
|
self.items[key] = entry
|
||||||
|
try:
|
||||||
|
with open(entry.path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to read cached thumbnail:', entry.path, as_unicode(err))
|
||||||
|
return None, None
|
||||||
|
return data, entry.timestamp
|
||||||
|
|
||||||
|
def invalidate(self, book_ids):
|
||||||
|
with self.lock:
|
||||||
|
if hasattr(self, 'total_size'):
|
||||||
|
for book_id in book_ids:
|
||||||
|
self._remove((self.group_id, book_id))
|
||||||
|
elif os.path.exists(self.location):
|
||||||
|
try:
|
||||||
|
raw = '\n'.join('%s %d' % (self.group_id, book_id) for book_id in book_ids)
|
||||||
|
with open(os.path.join(self.location, 'invalidate'), 'ab') as f:
|
||||||
|
f.write(raw.encode('ascii'))
|
||||||
|
except EnvironmentError as err:
|
||||||
|
self.log('Failed to write invalidate thumbnail record:', as_unicode(err))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_size(self):
|
||||||
|
with self.lock:
|
||||||
|
if not hasattr(self, 'total_size'):
|
||||||
|
self._load_index()
|
||||||
|
return self.total_size
|
||||||
|
|
||||||
|
def empty(self):
|
||||||
|
with self.lock:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(self.location, 'order'))
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
if not hasattr(self, 'total_size'):
|
||||||
|
self._load_index()
|
||||||
|
for entry in self.items.itervalues():
|
||||||
|
self._do_delete(entry.path)
|
||||||
|
self.total_size = 0
|
||||||
|
self.items = OrderedDict()
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def set_size(self, size_in_mb):
|
||||||
|
if size_in_mb <= self.min_disk_cache:
|
||||||
|
size_in_mb = 0
|
||||||
|
size_in_mb = max(0, size_in_mb)
|
||||||
|
with self.lock:
|
||||||
|
self.max_size = int(size_in_mb * (1024**2))
|
||||||
|
if hasattr(self, 'total_size'):
|
||||||
|
self._apply_size()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -115,6 +115,7 @@ defs['cover_grid_width'] = 0
|
|||||||
defs['cover_grid_height'] = 0
|
defs['cover_grid_height'] = 0
|
||||||
defs['cover_grid_color'] = (80, 80, 80)
|
defs['cover_grid_color'] = (80, 80, 80)
|
||||||
defs['cover_grid_cache_size'] = 200
|
defs['cover_grid_cache_size'] = 200
|
||||||
|
defs['cover_grid_disk_cache_size'] = 2000
|
||||||
defs['cover_grid_spacing'] = 0
|
defs['cover_grid_spacing'] = 0
|
||||||
defs['cover_grid_show_title'] = False
|
defs['cover_grid_show_title'] = False
|
||||||
del defs
|
del defs
|
||||||
|
@ -19,14 +19,29 @@ from PyQt4.Qt import (
|
|||||||
QTimer, QPalette, QColor, QItemSelection, QPixmap, QMenu, QApplication,
|
QTimer, QPalette, QColor, QItemSelection, QPixmap, QMenu, QApplication,
|
||||||
QMimeData, QUrl, QDrag, QPoint, QPainter, QRect, pyqtProperty, QEvent,
|
QMimeData, QUrl, QDrag, QPoint, QPainter, QRect, pyqtProperty, QEvent,
|
||||||
QPropertyAnimation, QEasingCurve, pyqtSlot, QHelpEvent, QAbstractItemView,
|
QPropertyAnimation, QEasingCurve, pyqtSlot, QHelpEvent, QAbstractItemView,
|
||||||
QStyleOptionViewItem, QToolTip)
|
QStyleOptionViewItem, QToolTip, QByteArray, QBuffer)
|
||||||
|
|
||||||
from calibre import fit_image
|
from calibre import fit_image, prints
|
||||||
from calibre.gui2 import gprefs, config
|
from calibre.gui2 import gprefs, config
|
||||||
from calibre.gui2.library.caches import CoverCache
|
from calibre.gui2.library.caches import CoverCache, ThumbnailCache
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
CM_TO_INCH = 0.393701
|
CM_TO_INCH = 0.393701
|
||||||
|
CACHE_FORMAT = 'PPM'
|
||||||
|
|
||||||
|
class EncodeError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def image_to_data(image): # {{{
|
||||||
|
ba = QByteArray()
|
||||||
|
buf = QBuffer(ba)
|
||||||
|
buf.open(QBuffer.WriteOnly)
|
||||||
|
if not image.save(buf, CACHE_FORMAT):
|
||||||
|
raise EncodeError('Failed to encode thumbnail')
|
||||||
|
ret = bytes(ba.data())
|
||||||
|
buf.close()
|
||||||
|
return ret
|
||||||
|
# }}}
|
||||||
|
|
||||||
# Drag 'n Drop {{{
|
# Drag 'n Drop {{{
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
@ -443,6 +458,8 @@ class GridView(QListView):
|
|||||||
self.setSpacing(self.delegate.spacing)
|
self.setSpacing(self.delegate.spacing)
|
||||||
self.set_color()
|
self.set_color()
|
||||||
self.ignore_render_requests = Event()
|
self.ignore_render_requests = Event()
|
||||||
|
self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'],
|
||||||
|
thumbnail_size=(self.delegate.cover_size.width(), self.delegate.cover_size.height()))
|
||||||
self.render_thread = None
|
self.render_thread = None
|
||||||
self.update_item.connect(self.re_render, type=Qt.QueuedConnection)
|
self.update_item.connect(self.re_render, type=Qt.QueuedConnection)
|
||||||
self.doubleClicked.connect(self.double_clicked)
|
self.doubleClicked.connect(self.double_clicked)
|
||||||
@ -485,12 +502,10 @@ class GridView(QListView):
|
|||||||
def slider_pressed(self):
|
def slider_pressed(self):
|
||||||
self.ignore_render_requests.set()
|
self.ignore_render_requests.set()
|
||||||
self.verticalScrollBar().valueChanged.connect(self.value_changed_during_scroll)
|
self.verticalScrollBar().valueChanged.connect(self.value_changed_during_scroll)
|
||||||
self.update_timer.setInterval(500)
|
|
||||||
|
|
||||||
def slider_released(self):
|
def slider_released(self):
|
||||||
self.update_viewport()
|
self.update_viewport()
|
||||||
self.verticalScrollBar().valueChanged.disconnect(self.value_changed_during_scroll)
|
self.verticalScrollBar().valueChanged.disconnect(self.value_changed_during_scroll)
|
||||||
self.update_timer.setInterval(200)
|
|
||||||
|
|
||||||
def value_changed_during_scroll(self):
|
def value_changed_during_scroll(self):
|
||||||
if self.ignore_render_requests.is_set():
|
if self.ignore_render_requests.is_set():
|
||||||
@ -545,9 +560,15 @@ class GridView(QListView):
|
|||||||
self.setSpacing(self.delegate.spacing)
|
self.setSpacing(self.delegate.spacing)
|
||||||
self.set_color()
|
self.set_color()
|
||||||
self.delegate.cover_cache.set_limit(gprefs['cover_grid_cache_size'])
|
self.delegate.cover_cache.set_limit(gprefs['cover_grid_cache_size'])
|
||||||
|
if size_changed:
|
||||||
|
self.thumbnail_cache.set_thumbnail_size(self.delegate.cover_size.width(), self.delegate.cover_size.height())
|
||||||
|
cs = gprefs['cover_grid_disk_cache_size']
|
||||||
|
if (cs*(1024**2)) != self.thumbnail_cache.max_size:
|
||||||
|
self.thumbnail_cache.set_size(cs)
|
||||||
|
|
||||||
def shown(self):
|
def shown(self):
|
||||||
if self.render_thread is None:
|
if self.render_thread is None:
|
||||||
|
self.thumbnail_cache.set_database(self.gui.current_db)
|
||||||
self.render_thread = Thread(target=self.render_covers)
|
self.render_thread = Thread(target=self.render_covers)
|
||||||
self.render_thread.daemon = True
|
self.render_thread.daemon = True
|
||||||
self.render_thread.start()
|
self.render_thread.start()
|
||||||
@ -572,19 +593,51 @@ class GridView(QListView):
|
|||||||
def render_cover(self, book_id):
|
def render_cover(self, book_id):
|
||||||
if self.ignore_render_requests.is_set():
|
if self.ignore_render_requests.is_set():
|
||||||
return
|
return
|
||||||
cdata = self.model().db.new_api.cover(book_id)
|
tcdata, timestamp = self.thumbnail_cache[book_id]
|
||||||
if cdata is not None:
|
use_cache = False
|
||||||
|
if timestamp is None:
|
||||||
|
# Not in cache
|
||||||
|
has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, 0)
|
||||||
|
else:
|
||||||
|
has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, timestamp)
|
||||||
|
if has_cover and cdata is None:
|
||||||
|
# The cached cover is fresh
|
||||||
|
cdata = tcdata
|
||||||
|
use_cache = True
|
||||||
|
|
||||||
|
if has_cover:
|
||||||
p = QImage()
|
p = QImage()
|
||||||
p.loadFromData(cdata)
|
p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG')
|
||||||
cdata = None
|
if p.isNull() and cdata is tcdata:
|
||||||
if not p.isNull():
|
# Invalid image in cache
|
||||||
width, height = p.width(), p.height()
|
self.thumbnail_cache.invalidate((book_id,))
|
||||||
scaled, nwidth, nheight = fit_image(width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height())
|
self.update_item.emit(book_id)
|
||||||
if scaled:
|
return
|
||||||
if self.ignore_render_requests.is_set():
|
cdata = None if p.isNull() else p
|
||||||
return
|
if not use_cache: # cache is stale
|
||||||
p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
if cdata is not None:
|
||||||
cdata = p
|
width, height = p.width(), p.height()
|
||||||
|
scaled, nwidth, nheight = fit_image(width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height())
|
||||||
|
if scaled:
|
||||||
|
if self.ignore_render_requests.is_set():
|
||||||
|
return
|
||||||
|
p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||||
|
cdata = p
|
||||||
|
# update cache
|
||||||
|
if cdata is None:
|
||||||
|
self.thumbnail_cache.invalidate((book_id,))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata))
|
||||||
|
except EncodeError as err:
|
||||||
|
self.thumbnail_cache.invalidate((book_id,))
|
||||||
|
prints(err)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
elif tcdata is not None:
|
||||||
|
# Cover was removed, but it exists in cache, remove from cache
|
||||||
|
self.thumbnail_cache.invalidate((book_id,))
|
||||||
self.delegate.cover_cache.set(book_id, cdata)
|
self.delegate.cover_cache.set(book_id, cdata)
|
||||||
self.update_item.emit(book_id)
|
self.update_item.emit(book_id)
|
||||||
|
|
||||||
@ -600,6 +653,7 @@ class GridView(QListView):
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.ignore_render_requests.set()
|
self.ignore_render_requests.set()
|
||||||
self.delegate.render_queue.put(None)
|
self.delegate.render_queue.put(None)
|
||||||
|
self.thumbnail_cache.shutdown()
|
||||||
|
|
||||||
def set_database(self, newdb, stage=0):
|
def set_database(self, newdb, stage=0):
|
||||||
if not hasattr(newdb, 'new_api'):
|
if not hasattr(newdb, 'new_api'):
|
||||||
@ -607,10 +661,12 @@ class GridView(QListView):
|
|||||||
if stage == 0:
|
if stage == 0:
|
||||||
self.ignore_render_requests.set()
|
self.ignore_render_requests.set()
|
||||||
try:
|
try:
|
||||||
self.model().db.new_api.remove_cover_cache(self.delegate.cover_cache)
|
for x in (self.delegate.cover_cache, self.thumbnail_cache):
|
||||||
|
self.model().db.new_api.remove_cover_cache(x)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass # db is None
|
pass # db is None
|
||||||
newdb.new_api.add_cover_cache(self.delegate.cover_cache)
|
for x in (self.delegate.cover_cache, self.thumbnail_cache):
|
||||||
|
newdb.new_api.add_cover_cache(x)
|
||||||
try:
|
try:
|
||||||
# Use a timeout so that if, for some reason, the render thread
|
# Use a timeout so that if, for some reason, the render thread
|
||||||
# gets stuck, we dont deadlock, future covers wont get
|
# gets stuck, we dont deadlock, future covers wont get
|
||||||
|
@ -11,6 +11,15 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from PyQt4.Qt import QImage, QPixmap
|
from PyQt4.Qt import QImage, QPixmap
|
||||||
|
|
||||||
|
from calibre.db.utils import ThumbnailCache as TC
|
||||||
|
|
||||||
|
class ThumbnailCache(TC):
|
||||||
|
def __init__(self, max_size=1024, thumbnail_size=(100, 100)):
|
||||||
|
TC.__init__(self, name='gui-thumbnail-cache', min_disk_cache=100, max_size=max_size, thumbnail_size=thumbnail_size)
|
||||||
|
|
||||||
|
def set_database(self, db):
|
||||||
|
TC.set_group_id(self, db.library_id)
|
||||||
|
|
||||||
class CoverCache(dict):
|
class CoverCache(dict):
|
||||||
|
|
||||||
' This is a RAM cache to speed up rendering of covers by storing them as QPixmaps '
|
' This is a RAM cache to speed up rendering of covers by storing them as QPixmaps '
|
||||||
@ -26,9 +35,10 @@ class CoverCache(dict):
|
|||||||
' Must be called in the GUI thread '
|
' Must be called in the GUI thread '
|
||||||
self.pixmap_staging = []
|
self.pixmap_staging = []
|
||||||
|
|
||||||
def invalidate(self, book_id):
|
def invalidate(self, book_ids):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self._pop(book_id)
|
for book_id in book_ids:
|
||||||
|
self._pop(book_id)
|
||||||
|
|
||||||
def _pop(self, book_id):
|
def _pop(self, book_id):
|
||||||
val = self.items.pop(book_id, None)
|
val = self.items.pop(book_id, None)
|
||||||
@ -75,3 +85,4 @@ class CoverCache(dict):
|
|||||||
for k in remove:
|
for k in remove:
|
||||||
self._pop(k)
|
self._pop(k)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,12 +5,15 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, QColorDialog,
|
from threading import Thread
|
||||||
QAbstractListModel, Qt, QIcon, QKeySequence, QPalette, QColor)
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, QColorDialog,
|
||||||
|
QAbstractListModel, Qt, QIcon, QKeySequence, QPalette, QColor, pyqtSignal)
|
||||||
|
|
||||||
|
from calibre import human_readable
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
||||||
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
||||||
from calibre.gui2 import config, gprefs, qt_app, NONE
|
from calibre.gui2 import config, gprefs, qt_app, NONE, open_local_file
|
||||||
from calibre.utils.localization import (available_translations,
|
from calibre.utils.localization import (available_translations,
|
||||||
get_language, get_lang)
|
get_language, get_lang)
|
||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
@ -95,6 +98,8 @@ class DisplayedFields(QAbstractListModel): # {{{
|
|||||||
|
|
||||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
|
size_calculated = pyqtSignal(object)
|
||||||
|
|
||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
db = gui.library_view.model().db
|
db = gui.library_view.model().db
|
||||||
@ -113,6 +118,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('cover_grid_width', gprefs)
|
r('cover_grid_width', gprefs)
|
||||||
r('cover_grid_height', gprefs)
|
r('cover_grid_height', gprefs)
|
||||||
r('cover_grid_cache_size', gprefs)
|
r('cover_grid_cache_size', gprefs)
|
||||||
|
r('cover_grid_disk_cache_size', gprefs)
|
||||||
r('cover_grid_spacing', gprefs)
|
r('cover_grid_spacing', gprefs)
|
||||||
r('cover_grid_show_title', gprefs)
|
r('cover_grid_show_title', gprefs)
|
||||||
|
|
||||||
@ -206,6 +212,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
for i in range(self.tabWidget.count()):
|
for i in range(self.tabWidget.count()):
|
||||||
if self.tabWidget.widget(i) is self.cover_grid_tab:
|
if self.tabWidget.widget(i) is self.cover_grid_tab:
|
||||||
self.tabWidget.removeTab(i)
|
self.tabWidget.removeTab(i)
|
||||||
|
self.size_calculated.connect(self.update_cg_cache_size, type=Qt.QueuedConnection)
|
||||||
|
self.tabWidget.currentChanged.connect(self.tab_changed)
|
||||||
|
self.cover_grid_empty_cache.clicked.connect(self.empty_cache)
|
||||||
|
self.cover_grid_open_cache.clicked.connect(self.open_cg_cache)
|
||||||
|
self.opt_cover_grid_disk_cache_size.setMinimum(self.gui.grid_view.thumbnail_cache.min_disk_cache)
|
||||||
|
self.opt_cover_grid_disk_cache_size.setMaximum(self.gui.grid_view.thumbnail_cache.min_disk_cache * 100)
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
@ -226,11 +238,34 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules')
|
self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules')
|
||||||
self.set_cg_color(gprefs['cover_grid_color'])
|
self.set_cg_color(gprefs['cover_grid_color'])
|
||||||
|
|
||||||
|
def open_cg_cache(self):
|
||||||
|
open_local_file(self.gui.grid_view.thumbnail_cache.location)
|
||||||
|
|
||||||
|
def update_cg_cache_size(self, size):
|
||||||
|
self.cover_grid_current_disk_cache.setText(
|
||||||
|
_('Current space used: %s') % human_readable(size))
|
||||||
|
|
||||||
|
def tab_changed(self, index):
|
||||||
|
if self.tabWidget.currentWidget() is self.cover_grid_tab:
|
||||||
|
self.show_current_cache_usage()
|
||||||
|
|
||||||
|
def show_current_cache_usage(self):
|
||||||
|
t = Thread(target=self.calc_cache_size)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def calc_cache_size(self):
|
||||||
|
self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_size)
|
||||||
|
|
||||||
def set_cg_color(self, val):
|
def set_cg_color(self, val):
|
||||||
pal = QPalette()
|
pal = QPalette()
|
||||||
pal.setColor(QPalette.Window, QColor(*val))
|
pal.setColor(QPalette.Window, QColor(*val))
|
||||||
self.cover_grid_color_label.setPalette(pal)
|
self.cover_grid_color_label.setPalette(pal)
|
||||||
|
|
||||||
|
def empty_cache(self):
|
||||||
|
self.gui.grid_view.thumbnail_cache.empty()
|
||||||
|
self.calc_cache_size()
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
ConfigWidgetBase.restore_defaults(self)
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
ofont = self.current_font
|
ofont = self.current_font
|
||||||
|
@ -231,147 +231,8 @@
|
|||||||
<attribute name="title">
|
<attribute name="title">
|
||||||
<string>Cover Grid</string>
|
<string>Cover Grid</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
<layout class="QFormLayout" name="formLayout">
|
<layout class="QGridLayout" name="gridLayout_4">
|
||||||
<item row="0" column="0" colspan="2">
|
<item row="4" column="1" colspan="3">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_11">
|
|
||||||
<property name="text">
|
|
||||||
<string>Cover &Width: </string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_cover_grid_width</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDoubleSpinBox" name="opt_cover_grid_width">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>The width of displayed covers</string>
|
|
||||||
</property>
|
|
||||||
<property name="specialValueText">
|
|
||||||
<string>Automatic</string>
|
|
||||||
</property>
|
|
||||||
<property name="suffix">
|
|
||||||
<string> cm</string>
|
|
||||||
</property>
|
|
||||||
<property name="decimals">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_12">
|
|
||||||
<property name="text">
|
|
||||||
<string>Cover &Height: </string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_cover_grid_height</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDoubleSpinBox" name="opt_cover_grid_height">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>The height of displayed covers</string>
|
|
||||||
</property>
|
|
||||||
<property name="specialValueText">
|
|
||||||
<string>Automatic</string>
|
|
||||||
</property>
|
|
||||||
<property name="suffix">
|
|
||||||
<string> cm</string>
|
|
||||||
</property>
|
|
||||||
<property name="decimals">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" colspan="2">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_14">
|
|
||||||
<property name="text">
|
|
||||||
<string>Background color for the cover grid:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>cover_grid_color_button</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="cover_grid_color_label">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>50</width>
|
|
||||||
<height>50</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="autoFillBackground">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="cover_grid_color_button">
|
|
||||||
<property name="text">
|
|
||||||
<string>Change &color</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_2">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="label_15">
|
|
||||||
<property name="text">
|
|
||||||
<string>Number of covers to cache in &memory:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_cover_grid_cache_size</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
|
||||||
<widget class="QSpinBox" name="opt_cover_grid_cache_size">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>The maximum number of covers to keep in memory. Increasing this will make rendering faster, at the cost of more memory usage.</string>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
|
||||||
<number>50000</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="QLabel" name="label_16">
|
<widget class="QLabel" name="label_16">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Spacing between covers:</string>
|
<string>&Spacing between covers:</string>
|
||||||
@ -381,7 +242,172 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="1" column="7" colspan="2">
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>417</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="3" colspan="3">
|
||||||
|
<widget class="QLabel" name="cover_grid_color_label">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>50</width>
|
||||||
|
<height>50</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0" colspan="9">
|
||||||
|
<widget class="QGroupBox" name="groupBox_3">
|
||||||
|
<property name="title">
|
||||||
|
<string>Caching of covers for improved performance</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="5">
|
||||||
|
<widget class="QLabel" name="label_13">
|
||||||
|
<property name="text">
|
||||||
|
<string>There are two kinds of caches that calibre uses to improve performance when rendering covers in the grid view. A disk cache that is kept on your hard disk and stores the cover thumbnails and an in memory cache used to ensure flicker free rendering of covers. For best results, keep the memory cache small and the disk cache large, unless you have a lot of extra RAM in your computer and dont mind it being used by the memory cache.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_15">
|
||||||
|
<property name="text">
|
||||||
|
<string>Number of covers to cache in &memory (keep this small):</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_cover_grid_cache_size</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QSpinBox" name="opt_cover_grid_cache_size">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The maximum number of covers to keep in memory. Increasing this will make rendering faster, at the cost of more memory usage.</string>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>50000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_18">
|
||||||
|
<property name="text">
|
||||||
|
<string>Maximum amount of disk space to use for caching thumbnails: </string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QSpinBox" name="opt_cover_grid_disk_cache_size">
|
||||||
|
<property name="specialValueText">
|
||||||
|
<string>Disable</string>
|
||||||
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string> MB</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="3" colspan="2">
|
||||||
|
<widget class="QLabel" name="cover_grid_current_disk_cache">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QPushButton" name="cover_grid_empty_cache">
|
||||||
|
<property name="text">
|
||||||
|
<string>Empty disk cache</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2" colspan="3">
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>310</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QPushButton" name="cover_grid_open_cache">
|
||||||
|
<property name="text">
|
||||||
|
<string>Open cache directory</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2" colspan="3">
|
||||||
|
<widget class="QLabel" name="label_12">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cover &Height: </string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_cover_grid_height</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="8">
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>365</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="6" colspan="2">
|
||||||
|
<widget class="QPushButton" name="cover_grid_color_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Change &color</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_11">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cover &Width: </string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_cover_grid_width</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="4" colspan="3">
|
||||||
<widget class="QDoubleSpinBox" name="opt_cover_grid_spacing">
|
<widget class="QDoubleSpinBox" name="opt_cover_grid_spacing">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The spacing between covers. A value of zero means calculate automatically based on cover size.</string>
|
<string>The spacing between covers. A value of zero means calculate automatically based on cover size.</string>
|
||||||
@ -397,13 +423,78 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="2" column="0" colspan="3">
|
||||||
|
<widget class="QLabel" name="label_14">
|
||||||
|
<property name="text">
|
||||||
|
<string>Background color for the cover grid:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>cover_grid_color_button</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QDoubleSpinBox" name="opt_cover_grid_width">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The width of displayed covers</string>
|
||||||
|
</property>
|
||||||
|
<property name="specialValueText">
|
||||||
|
<string>Automatic</string>
|
||||||
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string> cm</string>
|
||||||
|
</property>
|
||||||
|
<property name="decimals">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="5" colspan="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="opt_cover_grid_height">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The height of displayed covers</string>
|
||||||
|
</property>
|
||||||
|
<property name="specialValueText">
|
||||||
|
<string>Automatic</string>
|
||||||
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string> cm</string>
|
||||||
|
</property>
|
||||||
|
<property name="decimals">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="9">
|
||||||
<widget class="QCheckBox" name="opt_cover_grid_show_title">
|
<widget class="QCheckBox" name="opt_cover_grid_show_title">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show the book &title below the cover</string>
|
<string>Show the book &title below the cover</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="0" column="0" colspan="9">
|
||||||
|
<widget class="QLabel" name="label_19">
|
||||||
|
<property name="text">
|
||||||
|
<string>Control the cover grid view. You can enble this view by clicking the grid button in the bottom right corner of the main calibre window.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="tab_4">
|
<widget class="QWidget" name="tab_4">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user