02-10-2024, 05:32 AM | #76 |
Connoisseur
Posts: 57
Karma: 6698
Join Date: Sep 2022
Location: South Africa
Device: kindle pw10
|
|
03-05-2024, 11:30 AM | #77 |
Custom User Title
Posts: 8,767
Karma: 62032371
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Small question: I made an editor chain that runs a search-and-replace on the internal table of contents (to strip link anchors).
For the filter, has anybody ever seen that file named something other than toc.ncx? |
03-06-2024, 12:04 AM | #78 |
Fanatic
Posts: 516
Karma: 32106
Join Date: Feb 2012
Device: Onyx Boox Leaf
|
nav.xhtml
Sent from my Pixel 7 Pro using Tapatalk |
03-06-2024, 12:37 AM | #79 | |
Bibliophagist
Posts: 36,352
Karma: 145735552
Join Date: Jul 2010
Location: Vancouver
Device: Kobo Sage, Libra Colour, Lenovo M8 FHD, Paperwhite 4, Tolino epos
|
Quote:
I've actually needed to look at the .opf file to double check the filenames on occasion. |
|
03-06-2024, 10:25 AM | #80 |
Custom User Title
Posts: 8,767
Karma: 62032371
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Thank you both
|
03-09-2024, 12:30 PM | #81 |
Wizard
Posts: 1,108
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
|
Thanks for the info. I was operating under the impression that nav.xhtml is mandatory name. Will address this in the next version by looking for the name in the opf. The plugin already looks only for ncx extension regardless of the name for epub2.
|
03-31-2024, 11:52 AM | #82 |
Fanatic
Posts: 516
Karma: 32106
Join Date: Feb 2012
Device: Onyx Boox Leaf
|
I would like to request for "Download external resources" action.
Thank you for the plugin. 👍👍👍👍 Sent from my Pixel 7 Pro using Tapatalk |
04-02-2024, 10:56 AM | #83 | |
Wizard
Posts: 1,108
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
|
Quote:
Code:
import traceback from calibre.utils.localization import ngettext from polyglot.builtins import iteritems from calibre.ebooks.oeb.polish.download import ( download_external_resources, get_external_resources, replace_resources, ) from calibre_plugins.editor_chains.actions.base import EditorAction class ExternalResources(EditorAction): name = 'Download External Resources' headless = True def run(self, chain, settings, *args, **kwargs): container = chain.current_container try: resources = get_external_resources(container) except Exception as err: print(err, traceback.format_exc()) return if not resources: print(_('No external resources were found in this book.')) return try: downloaded = download_external_resources(container, resources) except Exception as err: print(_('Failed to download external resources.')) print(err, traceback.format_exc()) return replacements, failures = downloaded if failures: tb = [f'{url}\n\t{err}\n' for url, err in iteritems(failures)] det_msg='\n'.join(tb) if not replacements: print(_( 'Failed to download external resources.'), det_msg) return else: print(_( 'Warning: Failed to download some external resources.'), det_msg) try: ret = replace_resources(container, resources, replacements) except Exception as err: print(_('Failed to replace external resources')) print(err, traceback.format_exc()) return def validate(self, settings): return True |
|
04-26-2024, 07:44 PM | #84 |
Guru
Posts: 944
Karma: 1183425
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite
|
Hi, @capink.
Thanks for the great plugin! I've just created my first chain. It is a 'clean up' chain to correct the most common problems I usually find in my books. There were only two things that I missed and I was wondering if they could be added. 1) Add the 'Check book' action 2) Call other plugin's action (like running Epubcheck or ACE). Do you think this would be possible? |
04-27-2024, 01:00 AM | #85 | |
Wizard
Posts: 1,108
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
|
Quote:
Code:
#!/usr/bin/env python # ~*~ coding: utf-8 ~*~ from __future__ import (unicode_literals, division, absolute_import, print_function) import re # python 3 compatibility from six import text_type as unicode from PyQt5.Qt import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout, QDialog, QDialogButtonBox, QMenu, QPainter, QPoint, QPixmap, QSize, QIcon, QTreeWidgetItem, QTreeWidget, QAbstractItemView, QGroupBox, QCheckBox, QLabel, QAction) from calibre import prints from calibre.constants import DEBUG from calibre.gui2 import error_dialog from calibre_plugins.editor_chains.actions.base import EditorAction from calibre_plugins.editor_chains.common_utils import get_icon try: load_translations() except NameError: prints("EditorChains::actions/editor_actions.py - exception when loading translations") EXCLUDED_GROUPS = [ 'Editor Chains', 'Editor actions', 'Windows', 'Preview', 'Search' ] ICON_SIZE = 32 def group_unique_names(gui, group): menu_actions = [x for x in gui.__dict__.values() if isinstance(x, QAction)] menu_unique_names = [re.sub(r'^action\-', r'', x.objectName()) for x in menu_actions] if group == 'Plugins': unique_names = [x for x in gui.keyboard.groups['Plugins']] else: unique_names = [x for x in gui.keyboard.groups[group] if x in menu_unique_names] return unique_names def get_qaction_from_unique_name(gui, unique_name): shortcut = gui.keyboard.shortcuts[unique_name] ac = shortcut['action'] return ac def allowed_unique_names(gui): unique_names = [] for group in gui.keyboard.groups.keys(): unique_names.extend(group_unique_names(gui, group)) unique_names += [x for x in gui.keyboard.groups['Plugins']] return unique_names class Item(QTreeWidgetItem): pass class ConfigWidget(QWidget): def __init__(self, plugin_action): QWidget.__init__(self) self.plugin_action = plugin_action self.gui = plugin_action.gui self.all_nodes = {} # used to keep track of selected item and allow only one selection self.checked_item = None self._init_controls() def _init_controls(self): self.blank_icon = QIcon(I('blank.png')) l = self.l = QVBoxLayout() self.setLayout(l) # opts_groupbox = QGroupBox() # l.addWidget(opts_groupbox) # opts_groupbox_layout = QVBoxLayout() # opts_groupbox.setLayout(opts_groupbox_layout) # cursor_chk = self.cursor_chk = QCheckBox(_('Disable wait cursor for this action.')) # opts_groupbox_layout.addWidget(cursor_chk) # cursor_chk.setChecked(False) self.tv = QTreeWidget(self.gui) self.tv.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) self.tv.header().hide() self.tv.setSelectionMode(QAbstractItemView.SingleSelection) l.addWidget(self.tv, 1) self.tv.itemChanged.connect(self._tree_item_changed) self._populate_actions_tree() def _get_scaled_icon(self, icon): if icon.isNull(): return self.blank_icon # We need the icon scaled to 16x16 src = icon.pixmap(ICON_SIZE, ICON_SIZE) if src.width() == ICON_SIZE and src.height() == ICON_SIZE: return icon # Need a new version of the icon pm = QPixmap(ICON_SIZE, ICON_SIZE) pm.fill(Qt.transparent) p = QPainter(pm) p.drawPixmap(QPoint(int((ICON_SIZE - src.width()) / 2), int((ICON_SIZE - src.height()) / 2)), src) p.end() return QIcon(pm) def set_checked_state(self, item, col, state): item.setCheckState(col, state) if state == Qt.Checked: self.checked_item = item else: self.checked_item = None def _populate_actions_tree(self): self.top_level_items_map = {} for group in sorted(self.gui.keyboard.groups.keys()): if group in EXCLUDED_GROUPS: continue # Create a node for our top level plugin name tl = Item() tl.setText(0, group) # Normal top-level checkable plugin iaction handling tl.setFlags(Qt.ItemIsEnabled) #unique_names = self.gui.keyboard.groups[group] unique_names = group_unique_names(self.gui, group) # Iterate through all the children of this node self._populate_action_children(unique_names, tl) self.tv.addTopLevelItem(tl) def _populate_action_children(self, unique_names, parent): for unique_name in unique_names: ac = get_qaction_from_unique_name(self.gui, unique_name) text = ac.text().replace('&', '') it = Item(parent) it.setText(0, text) it.setData(0, Qt.UserRole, unique_name) it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) it.setCheckState(0, Qt.Unchecked) it.setIcon(0, self._get_scaled_icon(ac.icon())) self.all_nodes[unique_name] = it def _tree_item_changed(self, item, column): # Checkstate has been changed is_checked = item.checkState(column) == Qt.Checked #text = unicode(item.text(column)).replace('&', '') unique_name = item.data(column, Qt.UserRole) if is_checked: # un-check previously checked item if any if self.checked_item: self.tv.blockSignals(True) self.checked_item.setCheckState(0, Qt.Unchecked) self.tv.blockSignals(False) self.checked_item = item else: self.checked_item = None def _repopulate_actions_tree(self): self.tv.clear() self._populate_actions_tree() def load_settings(self, settings): if settings: #self.cursor_chk.setChecked(settings.get('disable_busy_cursor', False)) #self._repopulate_actions_tree() self.set_unique_name_checked(settings['unique_name']) def save_settings(self): settings = {} settings['unique_name'] = self.checked_item.data(0, Qt.UserRole) #settings['disable_busy_cursor'] = self.cursor_chk.isChecked() return settings def set_unique_name_checked(self, unique_name): it = self.all_nodes.get(unique_name) if it: self.set_checked_state(it, 0, Qt.Checked) self.tv.setCurrentItem(it) class EditorActions(EditorAction): name = 'Editor Actions' #_is_builtin = True def run(self, chain, settings): ac = get_qaction_from_unique_name(chain.gui, settings['unique_name']) if ac: if DEBUG: prints('Editor Chains: Editor Actions: Found action: {}'.format(ac.text().replace('&', ''))) cursor = Qt.WaitCursor if settings.get('disable_busy_cursor'): cursor = Qt.ArrowCursor QApplication.setOverrideCursor(cursor) try: ac.trigger() finally: QApplication.restoreOverrideCursor() if DEBUG: prints('Editor Chains: Editor Actions: Finishing run action') def validate(self, settings): gui = self.plugin_action.gui if not settings: return (_('Settings Error'), _('You must configure this action before running it')) if settings['unique_name'] not in allowed_unique_names(gui): return (_('Settings Error'), _('Action cannot be found: {}'.format(settings['unique_name']))) return True def config_widget(self): return ConfigWidget
The action provided above should enable you to call check books, but it will not allow for automatic repair, unless you click the hyperlink to do so. A more sophisticated version with options and automatic repair without clicking should be possible, I guess. But I am not familiar with that part of calibre code. If you are interested, you can add it as a custom action, through modules as in the action provided above. Hope that helps. Edit: Similar to Action Chains, this plugin provides an API for other plugins to provide actions for Editor Chains. A plugin developer needs the following to add actions:
Last edited by capink; 04-27-2024 at 05:12 AM. |
|
04-27-2024, 05:50 AM | #86 |
Guru
Posts: 944
Karma: 1183425
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite
|
Thank you very much. This custom action gave me full control of the editor!
Now, one last question: I've always wanted to automate merging splitted files (filename_split0001.xhtml, filename_split0002.xhtml, etc). With the custom action you provided, I can access the 'Merge files' action, but I didn't find a way to select the files based on a regular expression, just like when using 'Search and replace' and the filename filter. Is it possible? |
04-27-2024, 05:54 PM | #87 | |
Wizard
Posts: 1,108
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
|
Quote:
Code:
from qt.core import (QWidget, QVBoxLayout, QGroupBox) from calibre_plugins.editor_chains.actions.base import EditorAction from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless class ConfigWidget(QWidget): def __init__(self, plugin_action): QWidget.__init__(self) self.plugin_action = plugin_action self._init_controls() def _init_controls(self): l = self.l = QVBoxLayout() self.setLayout(l) scope_groupbox = QGroupBox(_('Files to select')) l.addWidget(scope_groupbox) scope_l = QVBoxLayout() scope_groupbox.setLayout(scope_l) self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical', headless=self.plugin_action.gui is None, hide=['selected-text','open','current']) scope_l.addWidget(self.where_box) l.addStretch(1) self.setMinimumSize(400,200) def load_settings(self, settings): if settings: self.where_box.where = settings['where'] def save_settings(self): settings = {} settings['where'] = self.where_box.where return settings class SelectFiles(EditorAction): name = 'Select Files' headless = True def run(self, chain, settings, *args, **kwargs): names = scope_names(chain, settings['where']) chain.gui.file_list.select_names(names) def validate(self, settings): scope_ok = validate_scope(settings['where']) if scope_ok is not True: return scope_ok return True def config_widget(self): return ConfigWidget def is_headless(self, settings): return scope_is_headless(settings['where']) Calibre uses a function called merge() to merge files. You can build an action around this function: Code:
from qt.core import (QWidget, QVBoxLayout, QGroupBox) from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.polish.split import merge from calibre.gui2.tweak_book import editors from calibre_plugins.editor_chains.actions.base import EditorAction from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless def spine_index(container, name): idx = 0 for spine_name, is_linear in container.spine_names: idx +=1 if name == spine_name: return idx class ConfigWidget(QWidget): def __init__(self, plugin_action): QWidget.__init__(self) self.plugin_action = plugin_action self._init_controls() def _init_controls(self): l = self.l = QVBoxLayout() self.setLayout(l) scope_groupbox = QGroupBox(_('Files to merge')) l.addWidget(scope_groupbox) scope_l = QVBoxLayout() scope_groupbox.setLayout(scope_l) self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical', headless=self.plugin_action.gui is None, hide=['selected-text','open','current']) scope_l.addWidget(self.where_box) l.addStretch(1) self.setMinimumSize(400,200) def load_settings(self, settings): if settings: self.where_box.where = settings['where'] def save_settings(self): settings = {} settings['where'] = self.where_box.where return settings class MergeFiles(EditorAction): name = 'Merge Files' headless = True def run(self, chain, settings, *args, **kwargs): names = scope_names(chain, settings['where']) print(f'DEBUG: Merging Files\n names: {names}') if len(names) < 2: print('Error: You need to select at least 2 files to merge') return container = chain.current_container spine_names = [name for name, is_linear in container.spine_names] if names[0] in set(container.manifest_items_of_type(OEB_DOCS)): category = 'text' elif names[0] in set(container.manifest_items_of_type(OEB_STYLES)): category = 'styles' else: print('Error: Can only merge files of categories: text or styles') return if category == 'text': if not set(names).issubset(set(container.manifest_items_of_type(OEB_DOCS))): print('Error: some files are your trying to merge are not of category text') return if not set(names).issubset(set(spine_names)): print('Error: Some files you are trying to merge not in spine') return names = sorted(names, key=lambda name: spine_index(container, name)) else: if not set(names).issubset(set(container.manifest_items_of_type(OEB_STYLES))): print('Error: some files are your trying to merge are not of category styles') return names = sorted(names) master = names[0] print(f'DEBUG: Merging Files\n master: {master}') merge(container, category, names, master) if master in editors: chain.boss.show_editor(master) def validate(self, settings): scope_ok = validate_scope(settings['where']) if scope_ok is not True: return scope_ok return True def config_widget(self): return ConfigWidget def is_headless(self, settings): return scope_is_headless(settings['where'])
|
|
04-28-2024, 09:10 AM | #88 |
Guru
Posts: 944
Karma: 1183425
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite
|
Thank you for this.
I'll adapt it to my needs. |
04-29-2024, 10:07 AM | #89 |
Guru
Posts: 944
Karma: 1183425
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite
|
Hi, @capink.
I noticed that the plugin does not remember the last size of the dialogs. Also, the 'Add actions' dialog throws an error when you right click on the table: Spoiler:
|
04-29-2024, 11:45 AM | #90 |
Wizard
Posts: 1,108
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
|
I uploaded a new version that should fix both issues. Please try and report back.
|
|
Similar Threads | ||||
Thread | Thread Starter | Forum | Replies | Last Post |
[Editor Plugin] LanguageTool | Doitsu | Plugins | 17 | 04-20-2024 02:21 PM |
[Editor Plugin] EpubCheck | Doitsu | Plugins | 146 | 09-07-2023 01:43 PM |
[Editor Plugin] - Enabling 'Customize plugin' dialog directly from the Editor | thiago.eec | Development | 7 | 01-09-2019 08:05 PM |
Sample Plugin for the Editor | DiapDealer | Editor | 77 | 12-10-2014 07:16 AM |
Editor plugin question | DiapDealer | Development | 2 | 07-28-2014 10:23 PM |