mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement the all files report
This commit is contained in:
parent
b33d0b735c
commit
e91994ab0e
@ -33,12 +33,17 @@ def get_category(name, mt):
|
|||||||
category = 'toc'
|
category = 'toc'
|
||||||
return category
|
return category
|
||||||
|
|
||||||
|
def safe_size(container, name):
|
||||||
|
try:
|
||||||
|
return os.path.getsize(container.name_to_abspath(name))
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
def file_data(container):
|
def file_data(container):
|
||||||
for name, path in container.name_path_map.iteritems():
|
for name, path in container.name_path_map.iteritems():
|
||||||
yield File(name, posixpath.dirname(name), os.path.getsize(name), posixpath.basename(name),
|
yield File(name, posixpath.dirname(name), posixpath.basename(name), safe_size(container, name),
|
||||||
get_category(name, container.mime_map.get(name, '')))
|
get_category(name, container.mime_map.get(name, '')))
|
||||||
|
|
||||||
|
|
||||||
def gather_data(container):
|
def gather_data(container):
|
||||||
data = {'files':tuple(file_data(container))}
|
data = {'files':tuple(file_data(container))}
|
||||||
return data
|
return data
|
||||||
|
@ -12,14 +12,63 @@ from operator import itemgetter
|
|||||||
|
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QSize, QStackedLayout, QLabel, QVBoxLayout, Qt, QWidget, pyqtSignal,
|
QSize, QStackedLayout, QLabel, QVBoxLayout, Qt, QWidget, pyqtSignal,
|
||||||
QAbstractTableModel)
|
QAbstractTableModel, QTableView, QSortFilterProxyModel, QIcon, QListWidget,
|
||||||
|
QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray)
|
||||||
|
|
||||||
|
from calibre import human_readable
|
||||||
|
from calibre.ebooks.oeb.polish.report import gather_data
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.tweak_book import current_container
|
from calibre.gui2.tweak_book import current_container, tprefs
|
||||||
from calibre.gui2.tweak_book.widgets import Dialog
|
from calibre.gui2.tweak_book.widgets import Dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.ebooks.oeb.polish.report import gather_data
|
from calibre.utils.icu import primary_contains, primary_sort_key
|
||||||
|
|
||||||
|
SORT_ROLE = Qt.UserRole
|
||||||
|
|
||||||
|
def read_state(name, default=None):
|
||||||
|
data = tprefs.get('reports-ui-state')
|
||||||
|
if data is None:
|
||||||
|
tprefs['reports-ui-state'] = data = {}
|
||||||
|
return data.get(name, default)
|
||||||
|
|
||||||
|
def save_state(name, val):
|
||||||
|
data = tprefs.get('reports-ui-state')
|
||||||
|
if isinstance(val, QByteArray):
|
||||||
|
val = bytearray(val)
|
||||||
|
if data is None:
|
||||||
|
tprefs['reports-ui-state'] = data = {}
|
||||||
|
data[name] = val
|
||||||
|
|
||||||
|
class ProxyModel(QSortFilterProxyModel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QSortFilterProxyModel.__init__(self, parent)
|
||||||
|
self._filter_text = None
|
||||||
|
|
||||||
|
def filter_text(self, text):
|
||||||
|
self._filter_text = text
|
||||||
|
self.setFilterFixedString(text)
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, row, parent):
|
||||||
|
if not self._filter_text:
|
||||||
|
return True
|
||||||
|
sm = self.sourceModel()
|
||||||
|
for item in (sm.data(sm.index(row, c, parent)) or '' for c in xrange(sm.columnCount())):
|
||||||
|
if item and primary_contains(self._filter_text, item):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def lessThan(self, left, right):
|
||||||
|
sm = self.sourceModel()
|
||||||
|
return sm.sort_key(left.row(), left.column()) < sm.sort_key(right.row(), right.column())
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||||
|
if orientation == Qt.Vertical and role == Qt.DisplayRole:
|
||||||
|
return section + 1
|
||||||
|
return QSortFilterProxyModel.headerData(self, section, orientation, role)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Files {{{
|
||||||
class FilesModel(QAbstractTableModel):
|
class FilesModel(QAbstractTableModel):
|
||||||
|
|
||||||
COLUMN_HEADERS = [_('Folder'), _('Name'), _('Size (KB)'), _('Type')]
|
COLUMN_HEADERS = [_('Folder'), _('Name'), _('Size (KB)'), _('Type')]
|
||||||
@ -33,8 +82,8 @@ class FilesModel(QAbstractTableModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
self.files = ()
|
self.files = self.sort_keys = ()
|
||||||
self.total_size = self.text_count = self.style_count = 0
|
self.total_size = self.images_size = self.fonts_size = 0
|
||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
|
|
||||||
def columnCount(self, parent=None):
|
def columnCount(self, parent=None):
|
||||||
@ -55,17 +104,32 @@ class FilesModel(QAbstractTableModel):
|
|||||||
self.beginResetModel()
|
self.beginResetModel()
|
||||||
self.files = data['files']
|
self.files = data['files']
|
||||||
self.total_size = sum(map(itemgetter(3), self.files))
|
self.total_size = sum(map(itemgetter(3), self.files))
|
||||||
self.text_count = sum(1 for f in self.files if f.category == 'text')
|
self.images_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'image')))
|
||||||
self.style_count = sum(1 for f in self.files if f.category == 'style')
|
self.fonts_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'font')))
|
||||||
|
psk = primary_sort_key
|
||||||
|
self.sort_keys = tuple((psk(entry.dir), psk(entry.basename), entry.size, psk(self.CATEGORY_NAMES.get(entry.category, '')))
|
||||||
|
for entry in self.files)
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
|
def sort_key(self, row, col):
|
||||||
|
try:
|
||||||
|
return self.sort_keys[row][col]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def name(self, index):
|
||||||
|
try:
|
||||||
|
return self.files[index.row()].name
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
def data(self, index, role=Qt.DisplayRole):
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
|
col = index.column()
|
||||||
try:
|
try:
|
||||||
entry = self.files[index.row()]
|
entry = self.files[index.row()]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
col = index.column()
|
|
||||||
if col == 0:
|
if col == 0:
|
||||||
return entry.dir
|
return entry.dir
|
||||||
if col == 1:
|
if col == 1:
|
||||||
@ -76,19 +140,111 @@ class FilesModel(QAbstractTableModel):
|
|||||||
if col == 3:
|
if col == 3:
|
||||||
return self.CATEGORY_NAMES.get(entry.category)
|
return self.CATEGORY_NAMES.get(entry.category)
|
||||||
|
|
||||||
|
class FilesWidget(QWidget):
|
||||||
|
|
||||||
|
edit_requested = pyqtSignal(object, object)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.l = l = QVBoxLayout(self)
|
||||||
|
|
||||||
|
self.filter_edit = e = QLineEdit(self)
|
||||||
|
l.addWidget(e)
|
||||||
|
e.setPlaceholderText(_('Filter'))
|
||||||
|
self.files = f = QTableView(self)
|
||||||
|
f.setSelectionBehavior(f.SelectRows), f.setSelectionMode(f.SingleSelection)
|
||||||
|
f.doubleClicked.connect(self.double_clicked)
|
||||||
|
self.model = m = FilesModel(self)
|
||||||
|
self.proxy = p = ProxyModel(self)
|
||||||
|
p.setSortRole(SORT_ROLE)
|
||||||
|
e.textChanged.connect(p.filter_text)
|
||||||
|
p.setSourceModel(m)
|
||||||
|
f.setModel(p)
|
||||||
|
l.addWidget(f)
|
||||||
|
f.setSortingEnabled(True)
|
||||||
|
|
||||||
|
self.summary = s = QLabel(self)
|
||||||
|
l.addWidget(s)
|
||||||
|
s.setText('\xa0')
|
||||||
|
try:
|
||||||
|
self.files.horizontalHeader().restoreState(read_state('all-files-table'))
|
||||||
|
except TypeError:
|
||||||
|
self.files.sortByColumn(1, Qt.AscendingOrder)
|
||||||
|
|
||||||
|
def __call__(self, data):
|
||||||
|
self.model(data)
|
||||||
|
m = self.model
|
||||||
|
self.summary.setText(_('Total uncompressed size of all files: {0} :: Images: {1} :: Fonts: {2}').format(*map(
|
||||||
|
human_readable, (m.total_size, m.images_size, m.fonts_size))))
|
||||||
|
|
||||||
|
def double_clicked(self, index):
|
||||||
|
name = self.model.name(index)
|
||||||
|
if name is not None:
|
||||||
|
self.edit_requested.emit(name, None)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
save_state('all-files-table', bytearray(self.files.horizontalHeader().saveState()))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Wrapper UI {{{
|
||||||
|
class ReportsWidget(QWidget):
|
||||||
|
|
||||||
|
edit_requested = pyqtSignal(object, object)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.l = QVBoxLayout(self)
|
||||||
|
self.splitter = l = QSplitter(self)
|
||||||
|
l.setChildrenCollapsible(False)
|
||||||
|
self.layout().addWidget(l)
|
||||||
|
self.reports = r = QListWidget(self)
|
||||||
|
l.addWidget(r)
|
||||||
|
self.stack = s = QStackedWidget(self)
|
||||||
|
l.addWidget(s)
|
||||||
|
r.currentRowChanged.connect(s.setCurrentIndex)
|
||||||
|
|
||||||
|
self.files = f = FilesWidget(self)
|
||||||
|
f.edit_requested.connect(self.edit_requested)
|
||||||
|
s.addWidget(f)
|
||||||
|
QListWidgetItem(_('Files'), r)
|
||||||
|
|
||||||
|
self.splitter.setStretchFactor(1, 500)
|
||||||
|
try:
|
||||||
|
self.splitter.restoreState(read_state('splitter-state'))
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
current_page = read_state('report-page')
|
||||||
|
if current_page is not None:
|
||||||
|
self.reports.setCurrentRow(current_page)
|
||||||
|
|
||||||
|
def __call__(self, data):
|
||||||
|
self.files(data)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
save_state('splitter-state', bytearray(self.splitter.saveState()))
|
||||||
|
save_state('report-page', self.reports.currentRow())
|
||||||
|
self.files.save()
|
||||||
|
|
||||||
class Reports(Dialog):
|
class Reports(Dialog):
|
||||||
|
|
||||||
data_gathered = pyqtSignal(object, object)
|
data_gathered = pyqtSignal(object, object)
|
||||||
|
edit_requested = pyqtSignal(object, object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
Dialog.__init__(self, _('Reports'), 'reports-dialog', parent=parent)
|
Dialog.__init__(self, _('Reports'), 'reports-dialog', parent=parent)
|
||||||
self.data_gathered.connect(self.display_data, type=Qt.QueuedConnection)
|
self.data_gathered.connect(self.display_data, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
self.wait_stack = s = QStackedLayout(self)
|
self.l = l = QVBoxLayout(self)
|
||||||
|
self.wait_stack = s = QStackedLayout()
|
||||||
|
l.addLayout(s)
|
||||||
|
l.addWidget(self.bb)
|
||||||
|
self.reports = r = ReportsWidget(self)
|
||||||
|
r.edit_requested.connect(self.edit_requested)
|
||||||
|
|
||||||
self.pw = pw = QWidget(self)
|
self.pw = pw = QWidget(self)
|
||||||
s.addWidget(pw)
|
s.addWidget(pw), s.addWidget(r)
|
||||||
pw.l = l = QVBoxLayout(pw)
|
pw.l = l = QVBoxLayout(pw)
|
||||||
self.pi = pi = ProgressIndicator(self, 256)
|
self.pi = pi = ProgressIndicator(self, 256)
|
||||||
l.addStretch(1), l.addWidget(pi, alignment=Qt.AlignHCenter), l.addSpacing(10)
|
l.addStretch(1), l.addWidget(pi, alignment=Qt.AlignHCenter), l.addSpacing(10)
|
||||||
@ -96,11 +252,17 @@ class Reports(Dialog):
|
|||||||
la.setStyleSheet('QLabel { font-size: 30pt; font-weight: bold }')
|
la.setStyleSheet('QLabel { font-size: 30pt; font-weight: bold }')
|
||||||
l.addWidget(la, alignment=Qt.AlignHCenter), l.addStretch(1)
|
l.addWidget(la, alignment=Qt.AlignHCenter), l.addStretch(1)
|
||||||
|
|
||||||
|
self.bb.setStandardButtons(self.bb.Close)
|
||||||
|
self.refresh_button = b = self.bb.addButton(_('&Refresh'), self.bb.ActionRole)
|
||||||
|
b.clicked.connect(self.refresh)
|
||||||
|
b.setIcon(QIcon(I('view-refresh')))
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return QSize(950, 600)
|
return QSize(950, 600)
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.wait_stack.setCurrentIndex(0)
|
self.wait_stack.setCurrentIndex(0)
|
||||||
|
self.setCursor(Qt.BusyCursor)
|
||||||
self.pi.startAnimation()
|
self.pi.startAnimation()
|
||||||
t = Thread(name='GatherReportData', target=self.gather_data)
|
t = Thread(name='GatherReportData', target=self.gather_data)
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
@ -116,11 +278,23 @@ class Reports(Dialog):
|
|||||||
|
|
||||||
def display_data(self, ok, data):
|
def display_data(self, ok, data):
|
||||||
self.wait_stack.setCurrentIndex(1)
|
self.wait_stack.setCurrentIndex(1)
|
||||||
|
self.unsetCursor()
|
||||||
|
self.pi.stopAnimation()
|
||||||
if not ok:
|
if not ok:
|
||||||
return error_dialog(self, _('Failed to gather data'), _(
|
return error_dialog(self, _('Failed to gather data'), _(
|
||||||
'Failed to gather data for the report. Click "Show details" for more'
|
'Failed to gather data for the report. Click "Show details" for more'
|
||||||
' information.'), det_msg=data, show=True)
|
' information.'), det_msg=data, show=True)
|
||||||
|
self.reports(data)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
with tprefs:
|
||||||
|
self.reports.save()
|
||||||
|
Dialog.accept(self)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.reports.save()
|
||||||
|
Dialog.reject(self)
|
||||||
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from calibre.gui2 import Application
|
from calibre.gui2 import Application
|
||||||
|
Loading…
x
Reference in New Issue
Block a user