mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Kobo driver: Allow specifying per-device model CSS
This commit is contained in:
parent
db7c986001
commit
92a801a71e
@ -36,7 +36,10 @@ from calibre.utils.config import Config, ConfigProxy, OptionParser, make_config_
|
|||||||
from polyglot.builtins import iteritems, itervalues
|
from polyglot.builtins import iteritems, itervalues
|
||||||
|
|
||||||
builtin_names = frozenset(p.name for p in builtin_plugins)
|
builtin_names = frozenset(p.name for p in builtin_plugins)
|
||||||
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
|
BLACKLISTED_PLUGINS = frozenset({
|
||||||
|
'Marvin XD',
|
||||||
|
'iOS reader applications',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def zip_value(iterable, value):
|
def zip_value(iterable, value):
|
||||||
|
@ -2,12 +2,22 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre.customize import Plugin
|
from calibre.customize import Plugin
|
||||||
|
|
||||||
|
|
||||||
|
class ModelMetadata(NamedTuple):
|
||||||
|
manufacturer_name: str
|
||||||
|
model_name: str
|
||||||
|
vendor_id: int
|
||||||
|
product_id: int
|
||||||
|
bcd: int
|
||||||
|
driver_class: type
|
||||||
|
|
||||||
|
|
||||||
class OpenPopupMessage:
|
class OpenPopupMessage:
|
||||||
|
|
||||||
def __init__(self, title='', message='', level='info', skip_dialog_skip_precheck=True):
|
def __init__(self, title='', message='', level='info', skip_dialog_skip_precheck=True):
|
||||||
@ -140,6 +150,11 @@ class DevicePlugin(Plugin):
|
|||||||
' GUI displays this as a non-modal popup. Should be an instance of OpenPopupMessage '
|
' GUI displays this as a non-modal popup. Should be an instance of OpenPopupMessage '
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_metadata(self) -> tuple[ModelMetadata, ...]:
|
||||||
|
' Metadata about all the actual device models this driver supports '
|
||||||
|
return ()
|
||||||
|
|
||||||
# Device detection {{{
|
# Device detection {{{
|
||||||
def test_bcd(self, bcdDevice, bcd):
|
def test_bcd(self, bcdDevice, bcd):
|
||||||
if bcd is None or len(bcd) == 0:
|
if bcd is None or len(bcd) == 0:
|
||||||
|
@ -22,6 +22,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from calibre import fsync, prints, strftime
|
from calibre import fsync, prints, strftime
|
||||||
from calibre.constants import DEBUG
|
from calibre.constants import DEBUG
|
||||||
|
from calibre.devices.interface import ModelMetadata
|
||||||
from calibre.devices.kobo.books import Book, ImageWrapper, KTCollectionsBookList
|
from calibre.devices.kobo.books import Book, ImageWrapper, KTCollectionsBookList
|
||||||
from calibre.devices.mime import mime_type_ext
|
from calibre.devices.mime import mime_type_ext
|
||||||
from calibre.devices.usbms.books import BookList, CollectionsBookList
|
from calibre.devices.usbms.books import BookList, CollectionsBookList
|
||||||
@ -2261,34 +2262,24 @@ class KOBOTOUCH(KOBO):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_extra_css(self):
|
def get_extra_css(self):
|
||||||
extra_sheet = None
|
css = ''
|
||||||
from css_parser.css import CSSRule
|
sheet = None
|
||||||
|
self.extra_css_options = {}
|
||||||
if self.modifying_css():
|
if self.modifying_css():
|
||||||
extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE)
|
extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE)
|
||||||
if os.path.exists(extra_css_path):
|
with suppress(FileNotFoundError), open(extra_css_path) as src:
|
||||||
from css_parser import parseFile as cssparseFile
|
css += '\n\n' + src.read()
|
||||||
try:
|
import json
|
||||||
extra_sheet = cssparseFile(extra_css_path)
|
pdcss = json.loads(self.get_pref('per_device_css') or '{}')
|
||||||
debug_print(f'KoboTouch:get_extra_css: Using extra CSS in {extra_css_path} ({len(extra_sheet.cssRules)} rules)')
|
if any_device := pdcss.get('pid=-1', ''):
|
||||||
if len(extra_sheet.cssRules) ==0:
|
css += '\n\n' + any_device
|
||||||
debug_print('KoboTouch:get_extra_css: Extra CSS file has no valid rules. CSS will not be modified.')
|
key = f'pid={self.detected_product_id()}'
|
||||||
extra_sheet = None
|
if device_css := pdcss.get(key, ''):
|
||||||
except Exception as e:
|
css += '\n\n' + device_css
|
||||||
debug_print(f'KoboTouch:get_extra_css: Problem parsing extra CSS file {extra_css_path}')
|
if css:
|
||||||
debug_print(f'KoboTouch:get_extra_css: Exception {e}')
|
from calibre.ebooks.oeb.polish.kepubify import check_if_css_needs_modification
|
||||||
|
sheet, self.extra_css_options['has_widows_orphans'], self.extra_css_options['has_atpage'] = check_if_css_needs_modification(css)
|
||||||
# create dictionary of features enabled in kobo extra css
|
return css, sheet
|
||||||
self.extra_css_options = {}
|
|
||||||
if extra_sheet:
|
|
||||||
# search extra_css for @page rule
|
|
||||||
self.extra_css_options['has_atpage'] = len(self.get_extra_css_rules(extra_sheet, CSSRule.PAGE_RULE)) > 0
|
|
||||||
|
|
||||||
# search extra_css for style rule(s) containing widows or orphans
|
|
||||||
self.extra_css_options['has_widows_orphans'] = len(self.get_extra_css_rules_widow_orphan(extra_sheet)) > 0
|
|
||||||
debug_print('KoboTouch:get_extra_css - CSS options:', self.extra_css_options)
|
|
||||||
|
|
||||||
return extra_sheet
|
|
||||||
|
|
||||||
def get_extra_css_rules(self, sheet, css_rule):
|
def get_extra_css_rules(self, sheet, css_rule):
|
||||||
return list(sheet.cssRules.rulesOfType(css_rule))
|
return list(sheet.cssRules.rulesOfType(css_rule))
|
||||||
@ -2308,7 +2299,7 @@ class KOBOTOUCH(KOBO):
|
|||||||
ext = '.' + name.rpartition('.')[-1].lower()
|
ext = '.' + name.rpartition('.')[-1].lower()
|
||||||
return ext == EPUB_EXT and modify_epub
|
return ext == EPUB_EXT and modify_epub
|
||||||
|
|
||||||
self.extra_sheet = self.get_extra_css()
|
self.extra_css, self.extra_sheet = self.get_extra_css()
|
||||||
modifiable = {x for x in names if should_modify(x)}
|
modifiable = {x for x in names if should_modify(x)}
|
||||||
self.files_to_rename_to_kepub = set()
|
self.files_to_rename_to_kepub = set()
|
||||||
if modifiable:
|
if modifiable:
|
||||||
@ -2321,7 +2312,7 @@ class KOBOTOUCH(KOBO):
|
|||||||
self.report_progress(i / float(len(modifiable)), 'Processing book: {} by {}'.format(mi.title, ' and '.join(mi.authors)))
|
self.report_progress(i / float(len(modifiable)), 'Processing book: {} by {}'.format(mi.title, ' and '.join(mi.authors)))
|
||||||
mi.kte_calibre_name = n
|
mi.kte_calibre_name = n
|
||||||
if self.get_pref('kepubify'):
|
if self.get_pref('kepubify'):
|
||||||
self._kepubify(file, n, mi, self.extra_sheet)
|
self._kepubify(file, n, mi)
|
||||||
else:
|
else:
|
||||||
self._modify_epub(file, mi)
|
self._modify_epub(file, mi)
|
||||||
i += 1
|
i += 1
|
||||||
@ -2360,17 +2351,19 @@ class KOBOTOUCH(KOBO):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _kepubify(self, path, name, mi, extra_css) -> None:
|
def _kepubify(self, path, name, mi) -> None:
|
||||||
from calibre.ebooks.oeb.polish.kepubify import kepubify_path, make_options
|
from calibre.ebooks.oeb.polish.kepubify import kepubify_path, make_options
|
||||||
debug_print(f'Starting conversion of {mi.title} ({name}) to kepub')
|
debug_print(f'Starting conversion of {mi.title} ({name}) to kepub')
|
||||||
opts = make_options(
|
opts = make_options(
|
||||||
extra_css=extra_css or '',
|
extra_css=self.extra_css or '',
|
||||||
affect_hyphenation=bool(self.get_pref('affect_hyphenation')),
|
affect_hyphenation=bool(self.get_pref('affect_hyphenation')),
|
||||||
disable_hyphenation=bool(self.get_pref('disable_hyphenation')),
|
disable_hyphenation=bool(self.get_pref('disable_hyphenation')),
|
||||||
hyphenation_min_chars=bool(self.get_pref('hyphenation_min_chars')),
|
hyphenation_min_chars=bool(self.get_pref('hyphenation_min_chars')),
|
||||||
hyphenation_min_chars_before=bool(self.get_pref('hyphenation_min_chars_before')),
|
hyphenation_min_chars_before=bool(self.get_pref('hyphenation_min_chars_before')),
|
||||||
hyphenation_min_chars_after=bool(self.get_pref('hyphenation_min_chars_after')),
|
hyphenation_min_chars_after=bool(self.get_pref('hyphenation_min_chars_after')),
|
||||||
hyphenation_limit_lines=bool(self.get_pref('hyphenation_limit_lines')),
|
hyphenation_limit_lines=bool(self.get_pref('hyphenation_limit_lines')),
|
||||||
|
remove_at_page_rules=self.extra_css_options.get('has_atpage', False),
|
||||||
|
remove_widows_and_orphans=self.extra_css_options.get('has_widows_orphans', False),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
kepubify_path(path, outpath=path, opts=opts, allow_overwrite=True)
|
kepubify_path(path, outpath=path, opts=opts, allow_overwrite=True)
|
||||||
@ -3671,6 +3664,7 @@ class KOBOTOUCH(KOBO):
|
|||||||
|
|
||||||
c.add_opt('kepubify', default=True)
|
c.add_opt('kepubify', default=True)
|
||||||
c.add_opt('modify_css', default=False)
|
c.add_opt('modify_css', default=False)
|
||||||
|
c.add_opt('per_device_css', default='{}')
|
||||||
c.add_opt('override_kobo_replace_existing', default=True) # Overriding the replace behaviour is how the driver has always worked.
|
c.add_opt('override_kobo_replace_existing', default=True) # Overriding the replace behaviour is how the driver has always worked.
|
||||||
|
|
||||||
c.add_opt('affect_hyphenation', default=False)
|
c.add_opt('affect_hyphenation', default=False)
|
||||||
@ -3695,6 +3689,39 @@ class KOBOTOUCH(KOBO):
|
|||||||
cls.opts = opts
|
cls.opts = opts
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_metadata(cls) -> tuple[ModelMetadata, ...]:
|
||||||
|
def m(name, pid, man='Kobo') -> ModelMetadata:
|
||||||
|
return ModelMetadata(man, name, cls.VENDOR_ID[-1], pid[-1], cls.BCD[-1], cls)
|
||||||
|
return (
|
||||||
|
m('Aura', cls.AURA_PRODUCT_ID),
|
||||||
|
m('Aura Edition 2', cls.AURA_EDITION2_PRODUCT_ID),
|
||||||
|
m('Aura HD', cls.AURA_HD_PRODUCT_ID),
|
||||||
|
m('Aura H2O', cls.AURA_H2O_PRODUCT_ID),
|
||||||
|
m('Aura H2O Edition 2', cls.AURA_H2O_EDITION2_PRODUCT_ID),
|
||||||
|
m('Aura One', cls.AURA_ONE_PRODUCT_ID),
|
||||||
|
m('Clara HD', cls.CLARA_HD_PRODUCT_ID),
|
||||||
|
m('Clara 2E', cls.CLARA_2E_PRODUCT_ID),
|
||||||
|
m('Clara Black and White', cls.CLARA_BW_PRODUCT_ID),
|
||||||
|
m('Clara Color', cls.CLARA_COLOR_PRODUCT_ID),
|
||||||
|
m('Elipsa', cls.ELIPSA_PRODUCT_ID),
|
||||||
|
m('Elipsa 2E', cls.ELIPSA_2E_PRODUCT_ID),
|
||||||
|
m('Forma', cls.FORMA_PRODUCT_ID),
|
||||||
|
m('Glo', cls.GLO_PRODUCT_ID),
|
||||||
|
m('Glo HD', cls.GLO_HD_PRODUCT_ID),
|
||||||
|
m('Libra H2O', cls.LIBRA_H2O_PRODUCT_ID),
|
||||||
|
m('Libra 2', cls.LIBRA2_PRODUCT_ID),
|
||||||
|
m('Mini', cls.MINI_PRODUCT_ID),
|
||||||
|
m('Nia', cls.NIA_PRODUCT_ID),
|
||||||
|
m('Sage', cls.SAGE_PRODUCT_ID),
|
||||||
|
m('Touch', cls.TOUCH_PRODUCT_ID),
|
||||||
|
m('Touch 2', cls.TOUCH2_PRODUCT_ID),
|
||||||
|
|
||||||
|
m('Shine 5', cls.TOLINO_SHINE_5THGEN_PRODUCT_ID, man='Tolino'),
|
||||||
|
m('Shine Color', cls.TOLINO_SHINE_COLOR_PRODUCT_ID, man='Tolino'),
|
||||||
|
m('Vision Color', cls.TOLINO_VISION_COLOR_PRODUCT_ID, man='Tolino'),
|
||||||
|
)
|
||||||
|
|
||||||
def is2024Device(self):
|
def is2024Device(self):
|
||||||
return self.detected_device.idProduct in self.LIBRA_COLOR_PRODUCT_ID
|
return self.detected_device.idProduct in self.LIBRA_COLOR_PRODUCT_ID
|
||||||
|
|
||||||
@ -3771,6 +3798,21 @@ class KOBOTOUCH(KOBO):
|
|||||||
def isShineColor(self):
|
def isShineColor(self):
|
||||||
return self.device_model_id.endswith('693') or self.detected_device.idProduct in self.TOLINO_SHINE_COLOR_PRODUCT_ID
|
return self.device_model_id.endswith('693') or self.detected_device.idProduct in self.TOLINO_SHINE_COLOR_PRODUCT_ID
|
||||||
|
|
||||||
|
def detected_product_id(self):
|
||||||
|
ans = self.detected_device.idProduct
|
||||||
|
if ans in self.LIBRA_COLOR_PRODUCT_ID:
|
||||||
|
mid = self.device_model_id[-3:]
|
||||||
|
match mid:
|
||||||
|
case '391':
|
||||||
|
ans = self.CLARA_BW_PRODUCT_ID[-1]
|
||||||
|
case '393':
|
||||||
|
ans = self.CLARA_COLOR_PRODUCT_ID[-1]
|
||||||
|
case '691':
|
||||||
|
ans = self.TOLINO_SHINE_5THGEN_PRODUCT_ID[-1]
|
||||||
|
case '693':
|
||||||
|
ans = self.TOLINO_SHINE_COLOR_PRODUCT_ID[-1]
|
||||||
|
return ans
|
||||||
|
|
||||||
def isTouch(self):
|
def isTouch(self):
|
||||||
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
|
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
|
||||||
|
|
||||||
|
@ -4,10 +4,29 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2015-2019, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015-2019, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import json
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from qt.core import QCheckBox, QDialog, QDialogButtonBox, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QVBoxLayout, QWidget
|
from qt.core import (
|
||||||
|
QCheckBox,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QGridLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
QSpinBox,
|
||||||
|
Qt,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from calibre.devices.interface import ModelMetadata
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.device_drivers.tabbed_device_config import DeviceConfigTab, DeviceOptionsGroupBox, TabbedDeviceConfig
|
from calibre.gui2.device_drivers.tabbed_device_config import DeviceConfigTab, DeviceOptionsGroupBox, TabbedDeviceConfig
|
||||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||||
@ -52,10 +71,12 @@ class KOBOTOUCHConfig(TabbedDeviceConfig):
|
|||||||
self.tab1 = Tab1Config(self, self.device)
|
self.tab1 = Tab1Config(self, self.device)
|
||||||
self.tab2 = Tab2Config(self, self.device)
|
self.tab2 = Tab2Config(self, self.device)
|
||||||
self.tab3 = Tab3Config(self, self.device)
|
self.tab3 = Tab3Config(self, self.device)
|
||||||
|
self.tab4 = Tab4Config(self, self.device)
|
||||||
|
|
||||||
self.addDeviceTab(self.tab1, _('Collections, covers && uploads'))
|
self.addDeviceTab(self.tab1, _('Collections, covers && uploads'))
|
||||||
self.addDeviceTab(self.tab2, _('Metadata, on device && advanced'))
|
self.addDeviceTab(self.tab2, _('Metadata, on device && advanced'))
|
||||||
self.addDeviceTab(self.tab3, _('Hyphenation'))
|
self.addDeviceTab(self.tab3, _('Hyphenation'))
|
||||||
|
self.addDeviceTab(self.tab4, _('Modify CSS'))
|
||||||
|
|
||||||
def get_pref(self, key):
|
def get_pref(self, key):
|
||||||
return self.device.get_pref(key)
|
return self.device.get_pref(key)
|
||||||
@ -132,6 +153,7 @@ class KOBOTOUCHConfig(TabbedDeviceConfig):
|
|||||||
p['bookstats_timetoread_lower_template'] = self.bookstats_timetoread_lower_template
|
p['bookstats_timetoread_lower_template'] = self.bookstats_timetoread_lower_template
|
||||||
|
|
||||||
p['modify_css'] = self.modify_css
|
p['modify_css'] = self.modify_css
|
||||||
|
p['per_device_css'] = self.per_device_css
|
||||||
p['kepubify'] = self.kepubify
|
p['kepubify'] = self.kepubify
|
||||||
p['override_kobo_replace_existing'] = self.override_kobo_replace_existing
|
p['override_kobo_replace_existing'] = self.override_kobo_replace_existing
|
||||||
|
|
||||||
@ -217,6 +239,108 @@ class Tab3Config(DeviceConfigTab): # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
class Tab4Config(DeviceConfigTab): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent, device):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.l = l = QVBoxLayout(self)
|
||||||
|
self.modify_css_options = h = ModifyCSSGroupBox(self, device)
|
||||||
|
self.addDeviceWidget(h)
|
||||||
|
l.addWidget(h)
|
||||||
|
l.addStretch()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
return self.modify_css_options.validate()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
class ModifyCSSGroupBox(DeviceOptionsGroupBox):
|
||||||
|
|
||||||
|
def __init__(self, parent, device):
|
||||||
|
super().__init__(parent, device)
|
||||||
|
self.setTitle(_('Modify CSS of books sent to the device'))
|
||||||
|
self.setCheckable(True)
|
||||||
|
self.setChecked(device.get_pref('modify_css'))
|
||||||
|
self.l = l = QVBoxLayout(self)
|
||||||
|
self.la = la = QLabel(
|
||||||
|
_('This allows addition of user CSS rules and removal of some CSS. '
|
||||||
|
'When sending a book, the driver adds the contents of {0} to all stylesheets in the book. '
|
||||||
|
'This file is searched for in the root folder of the main memory of the device. '
|
||||||
|
'As well as this, if the file contains settings for "orphans" or "widows", '
|
||||||
|
'these are removed from all styles in the original stylesheet.').format(device.KOBO_EXTRA_CSSFILE),
|
||||||
|
)
|
||||||
|
la.setWordWrap(True)
|
||||||
|
l.addWidget(la)
|
||||||
|
self.la2 = la = QLabel(_(
|
||||||
|
'Additionally, model specific CSS can be specified below:'))
|
||||||
|
la.setWordWrap(True)
|
||||||
|
l.addWidget(la)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pdcss = json.loads(device.get_pref('per_device_css') or '{}')
|
||||||
|
except Exception:
|
||||||
|
pdcss = {}
|
||||||
|
self.dev_list = QListWidget(self)
|
||||||
|
self.css_edit = QPlainTextEdit(self)
|
||||||
|
self.css_edit.setPlaceholderText(_('Enter the CSS to use for books on this model of device'))
|
||||||
|
self.css_edit.textChanged.connect(self.css_text_changed)
|
||||||
|
h = QHBoxLayout()
|
||||||
|
h.addWidget(self.dev_list), h.addWidget(self.css_edit, stretch=100)
|
||||||
|
l.addLayout(h)
|
||||||
|
for mm in [ModelMetadata('', _('All models'), -1, -1, -1, type(device))] + sorted(
|
||||||
|
device.model_metadata(), key=lambda x: x.model_name.lower()):
|
||||||
|
css = pdcss.get(f'pid={mm.product_id}', '')
|
||||||
|
i = QListWidgetItem(mm.model_name, self.dev_list)
|
||||||
|
i.setData(Qt.ItemDataRole.UserRole, (mm, css or ''))
|
||||||
|
self.dev_list.setCurrentRow(0)
|
||||||
|
self.dev_list.currentItemChanged.connect(self.current_device_changed)
|
||||||
|
self.current_device_changed()
|
||||||
|
self.clear_button = b = QPushButton(_('&Clear all model specific CSS'))
|
||||||
|
l.addWidget(b)
|
||||||
|
b.clicked.connect(self.clear_all_css)
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
for i in range(self.dev_list.count()):
|
||||||
|
yield self.dev_list.item(i)
|
||||||
|
|
||||||
|
def clear_all_css(self):
|
||||||
|
for item in self.items():
|
||||||
|
mm, css = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, (mm, ''))
|
||||||
|
self.current_device_changed()
|
||||||
|
|
||||||
|
def current_device_changed(self):
|
||||||
|
i = self.dev_list.currentItem()
|
||||||
|
css = ''
|
||||||
|
if i is not None:
|
||||||
|
mm, css = i.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self.css_edit.setPlainText(css or '')
|
||||||
|
|
||||||
|
def css_text_changed(self):
|
||||||
|
i = self.dev_list.currentItem()
|
||||||
|
if i is not None:
|
||||||
|
mm, css = i.data(Qt.ItemDataRole.UserRole)
|
||||||
|
css = self.css_edit.toPlainText().strip()
|
||||||
|
i.setData(Qt.ItemDataRole.UserRole, (mm, css))
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modify_css(self):
|
||||||
|
return self.isChecked()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def per_device_css(self):
|
||||||
|
ans = {}
|
||||||
|
for item in self.items():
|
||||||
|
mm, css = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
if css:
|
||||||
|
ans[f'pid={mm.product_id}'] = css
|
||||||
|
return json.dumps(ans)
|
||||||
|
|
||||||
|
|
||||||
class BookUploadsGroupBox(DeviceOptionsGroupBox):
|
class BookUploadsGroupBox(DeviceOptionsGroupBox):
|
||||||
|
|
||||||
def __init__(self, parent, device):
|
def __init__(self, parent, device):
|
||||||
@ -235,15 +359,6 @@ class BookUploadsGroupBox(DeviceOptionsGroupBox):
|
|||||||
' the Kobo viewer. If you would rather use the legacy viewer for EPUB, disable this option.'
|
' the Kobo viewer. If you would rather use the legacy viewer for EPUB, disable this option.'
|
||||||
), device.get_pref('kepubify'))
|
), device.get_pref('kepubify'))
|
||||||
|
|
||||||
self.modify_css_checkbox = create_checkbox(
|
|
||||||
_('Modify CSS'),
|
|
||||||
_('This allows addition of user CSS rules and removal of some CSS. '
|
|
||||||
'When sending a book, the driver adds the contents of {0} to all stylesheets in the book. '
|
|
||||||
'This file is searched for in the root folder of the main memory of the device. '
|
|
||||||
'As well as this, if the file contains settings for the "orphans" or "widows", '
|
|
||||||
'these are removed for all styles in the original stylesheet.').format(device.KOBO_EXTRA_CSSFILE),
|
|
||||||
device.get_pref('modify_css')
|
|
||||||
)
|
|
||||||
self.override_kobo_replace_existing_checkbox = create_checkbox(
|
self.override_kobo_replace_existing_checkbox = create_checkbox(
|
||||||
_('Do not treat replacements as new books'),
|
_('Do not treat replacements as new books'),
|
||||||
_('When a new book is side-loaded, the Kobo firmware imports details of the book into the internal database. '
|
_('When a new book is side-loaded, the Kobo firmware imports details of the book into the internal database. '
|
||||||
@ -256,12 +371,7 @@ class BookUploadsGroupBox(DeviceOptionsGroupBox):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.options_layout.addWidget(self.kepubify_checkbox, 0, 0, 1, 2)
|
self.options_layout.addWidget(self.kepubify_checkbox, 0, 0, 1, 2)
|
||||||
self.options_layout.addWidget(self.modify_css_checkbox, 1, 0, 1, 2)
|
self.options_layout.addWidget(self.override_kobo_replace_existing_checkbox, 1, 0, 1, 2)
|
||||||
self.options_layout.addWidget(self.override_kobo_replace_existing_checkbox, 2, 0, 1, 2)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def modify_css(self):
|
|
||||||
return self.modify_css_checkbox.isChecked()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def override_kobo_replace_existing(self):
|
def override_kobo_replace_existing(self):
|
||||||
|
@ -551,6 +551,25 @@ def unkepubify_path(path, outpath='', max_workers=0, allow_overwrite=False):
|
|||||||
return outpath
|
return outpath
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_css_needs_modification(extra_css: str) -> tuple[bool, bool]:
|
||||||
|
remove_widows_and_orphans = remove_at_page_rules = False
|
||||||
|
if extra_css:
|
||||||
|
try:
|
||||||
|
sheet = css_parser().parseString(extra_css)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for rule in sheet.cssRules:
|
||||||
|
if rule.type == CSSRule.PAGE_RULE:
|
||||||
|
remove_at_page_rules = True
|
||||||
|
elif rule.type == CSSRule.STYLE_RULE:
|
||||||
|
if rule.style['widows'] or rule.style['orphans']:
|
||||||
|
remove_widows_and_orphans = True
|
||||||
|
if remove_widows_and_orphans and remove_at_page_rules:
|
||||||
|
break
|
||||||
|
return sheet, remove_widows_and_orphans, remove_at_page_rules
|
||||||
|
|
||||||
|
|
||||||
def make_options(
|
def make_options(
|
||||||
extra_css: str = '',
|
extra_css: str = '',
|
||||||
affect_hyphenation: bool = False,
|
affect_hyphenation: bool = False,
|
||||||
@ -559,18 +578,12 @@ def make_options(
|
|||||||
hyphenation_min_chars_before: int = 3,
|
hyphenation_min_chars_before: int = 3,
|
||||||
hyphenation_min_chars_after: int = 3,
|
hyphenation_min_chars_after: int = 3,
|
||||||
hyphenation_limit_lines: int = 2,
|
hyphenation_limit_lines: int = 2,
|
||||||
|
|
||||||
|
remove_widows_and_orphans: bool | None = None,
|
||||||
|
remove_at_page_rules: bool | None = None,
|
||||||
) -> Options:
|
) -> Options:
|
||||||
remove_widows_and_orphans = remove_at_page_rules = False
|
if remove_widows_and_orphans is None or remove_at_page_rules is None:
|
||||||
if extra_css:
|
_, remove_widows_and_orphans, remove_at_page_rules = check_if_css_needs_modification(extra_css)
|
||||||
sheet = css_parser().parseString(extra_css)
|
|
||||||
for rule in sheet.cssRules:
|
|
||||||
if rule.type == CSSRule.PAGE_RULE:
|
|
||||||
remove_at_page_rules = True
|
|
||||||
elif rule.type == CSSRule.STYLE_RULE:
|
|
||||||
if rule.style['widows'] or rule.style['orphans']:
|
|
||||||
remove_widows_and_orphans = True
|
|
||||||
if remove_widows_and_orphans and remove_at_page_rules:
|
|
||||||
break
|
|
||||||
hyphen_css = ''
|
hyphen_css = ''
|
||||||
if affect_hyphenation:
|
if affect_hyphenation:
|
||||||
if disable_hyphenation:
|
if disable_hyphenation:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user