From abd1adb22f20822f95f2d6b77d65bb4e50521cee Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Wed, 3 Dec 2025 15:06:02 +0100 Subject: [PATCH 01/12] Add bulk metadata editing support * Enable multi-file selection in Metadata Editor using getOpenFileNames() * Disable Volume, Number, and Title fields in bulk mode * Pre-fill Series and author fields from first selected file in bulk mode * Show status "Editing Y files" in bulk mode * Add progress display during bulk save ("Processing X/Y: filename") * Disable buttons during processing * Skip read-only CBR files with error collection * Show error summary dialog if any files fail to save * Keep single file metadata editing the same --- kindlecomicconverter/KCC_gui.py | 184 +++++++++++++++++++++++--------- 1 file changed, 134 insertions(+), 50 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 5ec3371..e227f4b 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -617,27 +617,30 @@ class KCCGUI(KCC_ui.Ui_mainWindow): GUI.jobList.scrollToBottom() def selectFileMetaEditor(self, sname): + files = [] if not sname: if QApplication.keyboardModifiers() == Qt.ShiftModifier: dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) if dname != '': - sname = os.path.join(dname, 'ComicInfo.xml') - self.lastPath = os.path.dirname(sname) + files = [os.path.join(dname, 'ComicInfo.xml')] + self.lastPath = os.path.dirname(files[0]) else: if self.sevenzip: - fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cbr *.cb7)') + fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath, + 'Comic (*.cbz *.cbr *.cb7)') + files = fnames[0] + if files: + self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir)) else: - fname = [''] self.showDialog("Editor is disabled due to a lack of 7z.", 'error') self.addMessage('Install 7z (link)' ' to enable metadata editing.', 'warning') - if fname[0] != '': - sname = fname[0] - self.lastPath = os.path.abspath(os.path.join(sname, os.pardir)) - if sname: + else: + files = [sname] + + if files: try: - self.editor.loadData(sname) + self.editor.loadData(files) except Exception as err: _, _, traceback = sys.exc_info() GUI.sentry.captureException() @@ -1415,53 +1418,132 @@ class KCCGUI(KCC_ui.Ui_mainWindow): class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): - def loadData(self, file): - self.parser = metadata.MetadataParser(file) - if self.parser.format in ['RAR', 'RAR5']: - self.editorWidget.setEnabled(False) - self.okButton.setEnabled(False) - self.statusLabel.setText('CBR metadata are read-only.') - else: + def loadData(self, files): + self.files = files if isinstance(files, list) else [files] + self.bulkMode = len(self.files) > 1 + + if self.bulkMode: + firstFile = self.files[0] + self.parser = metadata.MetadataParser(firstFile) self.editorWidget.setEnabled(True) self.okButton.setEnabled(True) - self.statusLabel.setText('Separate authors with a comma.') - for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine): - field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) - for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): - field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) - for field in (self.seriesLine, self.titleLine): - if field.text() == '': - path = Path(file) - if file.endswith('.xml'): - field.setText(path.parent.name) - else: - field.setText(path.stem) + self.statusLabel.setText(f'Editing {len(self.files)} files.') + + for field in (self.volumeLine, self.numberLine, self.titleLine): + field.setEnabled(False) + field.setText('') + field.setPlaceholderText('(multiple files)') + + for field in (self.seriesLine,): + field.setEnabled(True) + field.setPlaceholderText('') + field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) + + for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): + field.setEnabled(True) + field.setPlaceholderText('') + field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) + else: + file = self.files[0] + self.parser = metadata.MetadataParser(file) + + for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine, + self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): + field.setEnabled(True) + field.setPlaceholderText('') + + if self.parser.format in ['RAR', 'RAR5']: + self.editorWidget.setEnabled(False) + self.okButton.setEnabled(False) + self.statusLabel.setText('CBR metadata are read-only.') + else: + self.editorWidget.setEnabled(True) + self.okButton.setEnabled(True) + self.statusLabel.setText('Separate authors with a comma.') + + for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine): + field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) + for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): + field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) + for field in (self.seriesLine, self.titleLine): + if field.text() == '': + path = Path(file) + if file.endswith('.xml'): + field.setText(path.parent.name) + else: + field.setText(path.stem) def saveData(self): - for field in (self.volumeLine, self.numberLine): - if field.text().isnumeric() or self.cleanData(field.text()) == '': - self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text()) - else: - self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.') - break - else: - for field in (self.seriesLine, self.titleLine): - self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text()) + if self.bulkMode: + bulkData = {} + if self.cleanData(self.seriesLine.text()): + bulkData['Series'] = self.cleanData(self.seriesLine.text()) + for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): + fieldName = field.objectName().capitalize()[:-4] + 's' values = self.cleanData(field.text()).split(',') - tmpData = [] - for value in values: - if self.cleanData(value) != '': - tmpData.append(self.cleanData(value)) - self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData - try: - self.parser.saveXML() - except Exception as err: - _, _, traceback = sys.exc_info() - GUI.sentry.captureException() - GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s" - % (str(err), sanitizeTrace(traceback)), 'error') + tmpData = [self.cleanData(v) for v in values if self.cleanData(v)] + if tmpData: + bulkData[fieldName] = tmpData + + if not bulkData: + self.statusLabel.setText('No changes to apply.') + return + + errors = [] + total = len(self.files) + self.okButton.setEnabled(False) + self.cancelButton.setEnabled(False) + + for i, file in enumerate(self.files, 1): + self.statusLabel.setText(f'Processing {i}/{total}: {os.path.basename(file)}') + QApplication.processEvents() + + try: + parser = metadata.MetadataParser(file) + if parser.format in ['RAR', 'RAR5']: + errors.append(f'{os.path.basename(file)}: CBR is read-only') + continue + for key, value in bulkData.items(): + parser.data[key] = value + parser.saveXML() + except Exception as err: + errors.append(f'{os.path.basename(file)}: {str(err)}') + + self.okButton.setEnabled(True) + self.cancelButton.setEnabled(True) + + if errors: + GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) + + (f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error') + else: + self.statusLabel.setText(f'Successfully updated {total} files.') self.ui.close() + else: + for field in (self.volumeLine, self.numberLine): + if field.text().isnumeric() or self.cleanData(field.text()) == '': + self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text()) + else: + self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.') + break + else: + for field in (self.seriesLine, self.titleLine): + self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text()) + for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): + values = self.cleanData(field.text()).split(',') + tmpData = [] + for value in values: + if self.cleanData(value) != '': + tmpData.append(self.cleanData(value)) + self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData + try: + self.parser.saveXML() + except Exception as err: + _, _, traceback = sys.exc_info() + GUI.sentry.captureException() + GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s" + % (str(err), sanitizeTrace(traceback)), 'error') + self.ui.close() def cleanData(self, s): return escape(s.strip()) @@ -1469,6 +1551,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): def __init__(self): self.ui = QDialog() self.parser = None + self.files = [] + self.bulkMode = False self.setupUi(self.ui) self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) self.okButton.clicked.connect(self.saveData) From a50db2e5ed47c1d1a01805b21e41bebe68b7ea70 Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Wed, 3 Dec 2025 15:38:29 +0100 Subject: [PATCH 02/12] Add bulk volume editing with rich tooltip * Add checkbox next to Volume field for enabling bulk volume editing (only in bulk mode) * Support single number (5), range (1-10), or comma list (1, 3, 5) input formats * Sort files alphabetically before volume assignment * Validate that volume count matches file count * Add rich HTML tooltip explaining the feature --- kindlecomicconverter/KCC_gui.py | 100 +++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index e227f4b..0465f77 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -22,7 +22,7 @@ import itertools from pathlib import Path from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) -from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog) +from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QCheckBox) from PySide6.QtNetwork import (QLocalSocket, QLocalServer) import os @@ -1422,6 +1422,9 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): self.files = files if isinstance(files, list) else [files] self.bulkMode = len(self.files) > 1 + # Sort files by name for consistent volume assignment + self.files.sort() + if self.bulkMode: firstFile = self.files[0] self.parser = metadata.MetadataParser(firstFile) @@ -1429,6 +1432,10 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): self.okButton.setEnabled(True) self.statusLabel.setText(f'Editing {len(self.files)} files.') + # Show bulk volume checkbox + self.bulkVolumeCheck.setVisible(True) + self.bulkVolumeCheck.setChecked(False) + for field in (self.volumeLine, self.numberLine, self.titleLine): field.setEnabled(False) field.setText('') @@ -1447,6 +1454,9 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): file = self.files[0] self.parser = metadata.MetadataParser(file) + # Hide bulk volume checkbox in single file mode + self.bulkVolumeCheck.setVisible(False) + for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine, self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): field.setEnabled(True) @@ -1486,7 +1496,16 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): if tmpData: bulkData[fieldName] = tmpData - if not bulkData: + # Handle bulk volume editing + volumes = None + if self.bulkVolumeCheck.isChecked(): + volumeText = self.volumeLine.text() + volumes, error = self.parseVolumeInput(volumeText, len(self.files)) + if error: + self.statusLabel.setText(error) + return + + if not bulkData and volumes is None: self.statusLabel.setText('No changes to apply.') return @@ -1506,6 +1525,9 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): continue for key, value in bulkData.items(): parser.data[key] = value + # Set volume if bulk volume editing is enabled + if volumes is not None: + parser.data['Volume'] = str(volumes[i - 1]) parser.saveXML() except Exception as err: errors.append(f'{os.path.basename(file)}: {str(err)}') @@ -1548,6 +1570,60 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): def cleanData(self, s): return escape(s.strip()) + def parseVolumeInput(self, text, fileCount): + """Parse volume input and return list of volume numbers. + Supports: single number (5), range (5-10), comma list (1,3,5) + Returns (volumes_list, error_message) tuple. + """ + text = text.strip() + if not text: + return None, None + + volumes = [] + + # Check if it's a range (e.g., "5-10") + if '-' in text and ',' not in text: + parts = text.split('-') + if len(parts) == 2: + try: + start = int(parts[0].strip()) + end = int(parts[1].strip()) + if start <= end: + volumes = list(range(start, end + 1)) + else: + return None, 'Invalid range: start > end' + except ValueError: + return None, 'Invalid range format' + # Check if it's a comma-separated list (e.g., "1,3,5") + elif ',' in text: + try: + volumes = [int(v.strip()) for v in text.split(',') if v.strip()] + except ValueError: + return None, 'Invalid list format' + # Single number - generate sequence starting from that number + else: + try: + start = int(text) + volumes = list(range(start, start + fileCount)) + except ValueError: + return None, 'Invalid number' + + # Validate count + if volumes and len(volumes) != fileCount: + return None, f'Volume count ({len(volumes)}) ≠ file count ({fileCount})' + + return volumes, None + + def toggleBulkVolume(self, checked): + """Toggle volume field enabled state based on checkbox.""" + self.volumeLine.setEnabled(checked) + if checked: + self.volumeLine.setText('') + self.volumeLine.setPlaceholderText('e.g., 5 or 1-10 or 1,3,5') + else: + self.volumeLine.setText('') + self.volumeLine.setPlaceholderText('(multiple files)') + def __init__(self): self.ui = QDialog() self.parser = None @@ -1555,6 +1631,26 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): self.bulkMode = False self.setupUi(self.ui) self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) + + # Create bulk volume editing widgets + self.bulkVolumeCheck = QCheckBox() + self.bulkVolumeCheck.setToolTip( + 'Bulk Volume Editing
' + 'Check this box to assign volume numbers to multiple files.

' + 'Input formats:
' + '5 → sequence starting from 5 (5, 6, 7...)
' + '1-10 → range from 1 to 10
' + '1, 3, 5 → specific values

' + 'Note: Files are sorted alphabetically before assignment.' + ) + self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume) + + # Add widget to the grid layout at row 1 (Volume row), column 2 + self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1) + + # Hide by default (only shown in bulk mode) + self.bulkVolumeCheck.setVisible(False) + self.okButton.clicked.connect(self.saveData) self.cancelButton.clicked.connect(self.ui.close) if sys.platform.startswith('linux'): From 73221d92beec1eae7394e7e8e595858699d4416b Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Fri, 5 Dec 2025 01:40:25 +0100 Subject: [PATCH 03/12] Move bulkVolumeCheck widget definition to MetaEditor.ui * Define bulkVolumeCheck QCheckBox in MetaEditor.ui instead of creating it programmatically in KCC_gui.py * Update tab order to include bulkVolumeCheck after volumeLine --- gui/MetaEditor.ui | 14 ++++++++++++++ kindlecomicconverter/KCC_gui.py | 19 +------------------ kindlecomicconverter/KCC_ui_editor.py | 19 +++++++++++++++---- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/gui/MetaEditor.ui b/gui/MetaEditor.ui index 3b453ad..cb8a517 100644 --- a/gui/MetaEditor.ui +++ b/gui/MetaEditor.ui @@ -62,6 +62,19 @@ + + + + false + + + <b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> → sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> → range from 1 to 10<br><code>1, 3, 5</code> → specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i> + + + + + + @@ -195,6 +208,7 @@ seriesLine volumeLine + bulkVolumeCheck titleLine numberLine writerLine diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 0465f77..56ef484 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -22,7 +22,7 @@ import itertools from pathlib import Path from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) -from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QCheckBox) +from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog) from PySide6.QtNetwork import (QLocalSocket, QLocalServer) import os @@ -1632,25 +1632,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): self.setupUi(self.ui) self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) - # Create bulk volume editing widgets - self.bulkVolumeCheck = QCheckBox() - self.bulkVolumeCheck.setToolTip( - 'Bulk Volume Editing
' - 'Check this box to assign volume numbers to multiple files.

' - 'Input formats:
' - '5 → sequence starting from 5 (5, 6, 7...)
' - '1-10 → range from 1 to 10
' - '1, 3, 5 → specific values

' - 'Note: Files are sorted alphabetically before assignment.' - ) self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume) - # Add widget to the grid layout at row 1 (Volume row), column 2 - self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1) - - # Hide by default (only shown in bulk mode) - self.bulkVolumeCheck.setVisible(False) - self.okButton.clicked.connect(self.saveData) self.cancelButton.clicked.connect(self.ui.close) if sys.platform.startswith('linux'): diff --git a/kindlecomicconverter/KCC_ui_editor.py b/kindlecomicconverter/KCC_ui_editor.py index 7bc559d..07ec940 100644 --- a/kindlecomicconverter/KCC_ui_editor.py +++ b/kindlecomicconverter/KCC_ui_editor.py @@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QFontDatabase, QGradient, QIcon, QImage, QKeySequence, QLinearGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout, - QLabel, QLineEdit, QPushButton, QSizePolicy, - QVBoxLayout, QWidget) +from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout, + QHBoxLayout, QLabel, QLineEdit, QPushButton, + QSizePolicy, QVBoxLayout, QWidget) from . import KCC_rc class Ui_editorDialog(object): @@ -57,6 +57,12 @@ class Ui_editorDialog(object): self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1) + self.bulkVolumeCheck = QCheckBox(self.editorWidget) + self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck") + self.bulkVolumeCheck.setVisible(False) + + self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1) + self.label_3 = QLabel(self.editorWidget) self.label_3.setObjectName(u"label_3") @@ -157,7 +163,8 @@ class Ui_editorDialog(object): self.verticalLayout.addWidget(self.optionWidget) QWidget.setTabOrder(self.seriesLine, self.volumeLine) - QWidget.setTabOrder(self.volumeLine, self.titleLine) + QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck) + QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine) QWidget.setTabOrder(self.titleLine, self.numberLine) QWidget.setTabOrder(self.numberLine, self.writerLine) QWidget.setTabOrder(self.writerLine, self.pencillerLine) @@ -175,6 +182,10 @@ class Ui_editorDialog(object): editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None)) self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None)) self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None)) +#if QT_CONFIG(tooltip) + self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"Bulk Volume Editing
Check this box to assign volume numbers to multiple files.

Input formats:
5 \u2192 sequence starting from 5 (5, 6, 7...)
1-10 \u2192 range from 1 to 10
1, 3, 5 \u2192 specific values

Note: Files are sorted alphabetically before assignment.", None)) +#endif // QT_CONFIG(tooltip) + self.bulkVolumeCheck.setText("") self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None)) self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None)) self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None)) From 28e1410f50dba57a8df090767f4cb64e39c282fa Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Wed, 10 Dec 2025 03:03:56 +0100 Subject: [PATCH 04/12] Add multi-directory selection for bulk metadata editing * Shift+click on editor button now opens multi-directory selection dialog * Multiple directories enable bulk mode with volume number assignment * Uses Qt's built-in dialog with multi-selection enabled for native look and feel --- kindlecomicconverter/KCC_gui.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 56ef484..51fc97a 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -22,7 +22,7 @@ import itertools from pathlib import Path from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) -from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog) +from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView) from PySide6.QtNetwork import (QLocalSocket, QLocalServer) import os @@ -620,10 +620,25 @@ class KCCGUI(KCC_ui.Ui_mainWindow): files = [] if not sname: if QApplication.keyboardModifiers() == Qt.ShiftModifier: - dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) - if dname != '': - files = [os.path.join(dname, 'ComicInfo.xml')] - self.lastPath = os.path.dirname(files[0]) + # Multi-directory selection for bulk editing ComicInfo.xml + dialog = QFileDialog(MW, 'Select volume directories', self.lastPath) + dialog.setFileMode(QFileDialog.FileMode.Directory) + dialog.setOption(QFileDialog.Option.ShowDirsOnly, True) + dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True) + + # Enable multi-selection in the dialog (may not work with native dialog on all platforms) + file_view = dialog.findChild(QListView, 'listView') + if file_view: + file_view.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) + file_tree = dialog.findChild(QTreeView) + if file_tree: + file_tree.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) + + if dialog.exec(): + selected_dirs = dialog.selectedFiles() + if selected_dirs: + files = [os.path.join(d, 'ComicInfo.xml') for d in selected_dirs] + self.lastPath = os.path.dirname(selected_dirs[0]) else: if self.sevenzip: fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath, From 1bd2073c809ba277831e990cc93845af967a3fed Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Thu, 11 Dec 2025 21:45:10 +0100 Subject: [PATCH 05/12] Fix volume input validation in bulk metadata editor * Handle invalid range formats like "1-2-3" or "--" that previously fell through, now returns an error message * Add explicit check for empty/malformed range parts before attempting to parse * Add positive number validation for all input types (range, comma-list, single number) to be consistent with single file mode which uses isnumeric() * Add explicit validation for empty volumes list after parsing --- kindlecomicconverter/KCC_gui.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 51fc97a..ff89800 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1599,32 +1599,40 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): # Check if it's a range (e.g., "5-10") if '-' in text and ',' not in text: parts = text.split('-') - if len(parts) == 2: - try: - start = int(parts[0].strip()) - end = int(parts[1].strip()) - if start <= end: - volumes = list(range(start, end + 1)) - else: - return None, 'Invalid range: start > end' - except ValueError: - return None, 'Invalid range format' + if len(parts) != 2 or not parts[0].strip() or not parts[1].strip(): + return None, 'Invalid range format (use start-end)' + try: + start = int(parts[0].strip()) + end = int(parts[1].strip()) + if start < 0 or end < 0: + return None, 'Volume numbers must be positive' + if start > end: + return None, 'Invalid range: start > end' + volumes = list(range(start, end + 1)) + except ValueError: + return None, 'Invalid range format' # Check if it's a comma-separated list (e.g., "1,3,5") elif ',' in text: try: volumes = [int(v.strip()) for v in text.split(',') if v.strip()] + if any(v < 0 for v in volumes): + return None, 'Volume numbers must be positive' except ValueError: return None, 'Invalid list format' # Single number - generate sequence starting from that number else: try: start = int(text) + if start < 0: + return None, 'Volume number must be positive' volumes = list(range(start, start + fileCount)) except ValueError: return None, 'Invalid number' # Validate count - if volumes and len(volumes) != fileCount: + if not volumes: + return None, 'No valid volume numbers parsed' + if len(volumes) != fileCount: return None, f'Volume count ({len(volumes)}) ≠ file count ({fileCount})' return volumes, None From 9de0728b0e9e61e3f6e77f9bf2ff5dee0c90896e Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Fri, 12 Dec 2025 00:02:43 +0100 Subject: [PATCH 06/12] Keep metadata editor dialog open when error occur during bulks save --- kindlecomicconverter/KCC_gui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index ff89800..cc01bc9 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1553,9 +1553,10 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): if errors: GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) + (f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error') + self.statusLabel.setText('Errors occurred.') else: self.statusLabel.setText(f'Successfully updated {total} files.') - self.ui.close() + self.ui.close() else: for field in (self.volumeLine, self.numberLine): if field.text().isnumeric() or self.cleanData(field.text()) == '': From 61a4e44921f5a321273ffc9493825d6f1eb08753 Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Fri, 12 Dec 2025 01:29:55 +0100 Subject: [PATCH 07/12] Fix CBR read-only check in bulk metadata editor * Check all files for CBR format during load instead of during save * Unify the CBR check for single and bulk mode --- kindlecomicconverter/KCC_gui.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index cc01bc9..54616da 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1440,6 +1440,15 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): # Sort files by name for consistent volume assignment self.files.sort() + # Unified CBR check for all files (both single and bulk mode) + for file in self.files: + parser = metadata.MetadataParser(file) + if parser.format in ['RAR', 'RAR5']: + self.editorWidget.setEnabled(False) + self.okButton.setEnabled(False) + self.statusLabel.setText('CBR files in selection are read-only.') + return + if self.bulkMode: firstFile = self.files[0] self.parser = metadata.MetadataParser(firstFile) @@ -1477,14 +1486,9 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): field.setEnabled(True) field.setPlaceholderText('') - if self.parser.format in ['RAR', 'RAR5']: - self.editorWidget.setEnabled(False) - self.okButton.setEnabled(False) - self.statusLabel.setText('CBR metadata are read-only.') - else: - self.editorWidget.setEnabled(True) - self.okButton.setEnabled(True) - self.statusLabel.setText('Separate authors with a comma.') + self.editorWidget.setEnabled(True) + self.okButton.setEnabled(True) + self.statusLabel.setText('Separate authors with a comma.') for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine): field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) From 3bb7adb8d44bc28a57087077da626a8700ed4291 Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Fri, 12 Dec 2025 04:13:55 +0100 Subject: [PATCH 08/12] Handle mixed metadata values in bulk editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * In bulk mode, compare Series + Writer/Penciller/Inker/Colorist across all selected files (instead of using only the first file) * When values differ, show “(multiple values)” placeholder for that field * Add hover tooltip with overwrite warning and a File|Value table (or Value|Count summary when there are >20 files) --- kindlecomicconverter/KCC_gui.py | 101 ++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 54616da..50f290b 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1433,6 +1433,64 @@ class KCCGUI(KCC_ui.Ui_mainWindow): class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): + def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile): + """Build HTML tooltip for bulk metadata mismatch fields. + + valuesByFile: list of (file_path, value_str) + - if <= 20 rows: show File | Value + - else: show Value | Count + """ + note = '

Note: Changing this field will overwrite all values in all selected files.

' + + if len(valuesByFile) <= 20: + rows = ''.join( + '' + f'{escape(os.path.basename(f))}' + f'{escape(v)}' + '' + for f, v in valuesByFile + ) + + table = ( + '' + '' + '' + '' + '' + f'{rows}' + '
FileValue
' + ) + else: + counts = {} + for _, v in valuesByFile: + counts[v] = counts.get(v, 0) + 1 + + rows = ''.join( + '' + f'{escape(v)}' + f'{c}' + '' + for v, c in sorted(counts.items(), key=lambda t: (-t[1], t[0])) + ) + + table = ( + '' + '' + '' + '' + '' + f'{rows}' + '
ValueCount
' + ) + + tooltipHTML = f'\ + {escape(fieldLabel)}\ + {note}\ + {table}\ + ' + + return tooltipHTML + def loadData(self, files): self.files = files if isinstance(files, list) else [files] self.bulkMode = len(self.files) > 1 @@ -1455,25 +1513,44 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): self.editorWidget.setEnabled(True) self.okButton.setEnabled(True) self.statusLabel.setText(f'Editing {len(self.files)} files.') - + # Show bulk volume checkbox self.bulkVolumeCheck.setVisible(True) self.bulkVolumeCheck.setChecked(False) - + for field in (self.volumeLine, self.numberLine, self.titleLine): field.setEnabled(False) field.setText('') field.setPlaceholderText('(multiple files)') - - for field in (self.seriesLine,): - field.setEnabled(True) - field.setPlaceholderText('') - field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) - - for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): - field.setEnabled(True) - field.setPlaceholderText('') - field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) + field.setToolTip('') + + # Load metadata for all files and show common values, or “(multiple values)” + tooltip. + parsed = [] + for file in self.files: + parsed.append((file, metadata.MetadataParser(file))) + + field_specs = [ + (self.seriesLine, 'Series', lambda p: (p.data.get('Series', '') or '')), + (self.writerLine, 'Writer', lambda p: ', '.join(p.data.get('Writers', []) or [])), + (self.pencillerLine, 'Penciller', lambda p: ', '.join(p.data.get('Pencillers', []) or [])), + (self.inkerLine, 'Inker', lambda p: ', '.join(p.data.get('Inkers', []) or [])), + (self.coloristLine, 'Colorist', lambda p: ', '.join(p.data.get('Colorists', []) or [])), + ] + + for line, label, extractor in field_specs: + line.setEnabled(True) + valuesByFile = [(f, extractor(p)) for f, p in parsed] + uniqueValues = {v for _, v in valuesByFile} + + if len(uniqueValues) == 1: + common_value = valuesByFile[0][1] if valuesByFile else '' + line.setPlaceholderText('') + line.setToolTip('') + line.setText(common_value) + else: + line.setText('') + line.setPlaceholderText('(multiple values)') + line.setToolTip(self._buildBulkFieldToolTip(label, valuesByFile)) else: file = self.files[0] self.parser = metadata.MetadataParser(file) From 1df868ddb38538ca273b02397ba1b3612d492c91 Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Fri, 12 Dec 2025 04:16:22 +0100 Subject: [PATCH 09/12] Code comments cleanup --- kindlecomicconverter/KCC_gui.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 50f290b..fd7d083 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1434,12 +1434,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow): class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile): - """Build HTML tooltip for bulk metadata mismatch fields. - - valuesByFile: list of (file_path, value_str) - - if <= 20 rows: show File | Value - - else: show Value | Count - """ note = '

Note: Changing this field will overwrite all values in all selected files.

' if len(valuesByFile) <= 20: @@ -1668,10 +1662,6 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): return escape(s.strip()) def parseVolumeInput(self, text, fileCount): - """Parse volume input and return list of volume numbers. - Supports: single number (5), range (5-10), comma list (1,3,5) - Returns (volumes_list, error_message) tuple. - """ text = text.strip() if not text: return None, None @@ -1720,7 +1710,6 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): return volumes, None def toggleBulkVolume(self, checked): - """Toggle volume field enabled state based on checkbox.""" self.volumeLine.setEnabled(checked) if checked: self.volumeLine.setText('') From f915522f561b52d8f2b38b152dba2e0800eb83db Mon Sep 17 00:00:00 2001 From: jaroslawjanas Date: Sat, 13 Dec 2025 00:47:41 +0100 Subject: [PATCH 10/12] Use ExtendedSelection for multi-directory file dialog --- kindlecomicconverter/KCC_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index fd7d083..093a761 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -629,10 +629,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow): # Enable multi-selection in the dialog (may not work with native dialog on all platforms) file_view = dialog.findChild(QListView, 'listView') if file_view: - file_view.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) + file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) file_tree = dialog.findChild(QTreeView) if file_tree: - file_tree.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) + file_tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) if dialog.exec(): selected_dirs = dialog.selectedFiles() From 2e65bee7b2099439978e0cfda8bc90b00c8c2d99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 17:05:33 -0800 Subject: [PATCH 11/12] Bump actions/upload-artifact from 5 to 6 (#1196) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/package-linux.yml | 2 +- .github/workflows/package-macos.yml | 2 +- .github/workflows/package-osx-legacy.yml | 2 +- .github/workflows/package-windows.yml | 2 +- .github/workflows/package-windows7.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index afa1c42..72b95a4 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -59,7 +59,7 @@ jobs: env: UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync - name: upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: AppImage path: './*.AppImage*' diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index a132ce0..66187a6 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -80,7 +80,7 @@ jobs: run: | python setup.py build_binary - name: upload build - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: mac-os-build-${{ runner.arch }} path: dist/*.dmg diff --git a/.github/workflows/package-osx-legacy.yml b/.github/workflows/package-osx-legacy.yml index 2422229..086e7f4 100644 --- a/.github/workflows/package-osx-legacy.yml +++ b/.github/workflows/package-osx-legacy.yml @@ -51,7 +51,7 @@ jobs: run: | python3 setup.py build_binary - name: upload build - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: osx-build-${{ runner.arch }} path: dist/*.dmg diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index 1d4bf92..34148a9 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -53,7 +53,7 @@ jobs: python setup.py ${{ matrix.command }} - name: upload-unsigned-artifact id: upload-unsigned-artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: windows-build-${{ matrix.entry }} path: dist/*.exe diff --git a/.github/workflows/package-windows7.yml b/.github/workflows/package-windows7.yml index cfa2df0..9ea0782 100644 --- a/.github/workflows/package-windows7.yml +++ b/.github/workflows/package-windows7.yml @@ -46,7 +46,7 @@ jobs: python setup.py build_binary - name: upload-unsigned-artifact id: upload-unsigned-artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: windows7-build path: dist/*.exe From f31ae43b3465a5b522401e65688f9643a4f1038a Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Mon, 18 May 2026 09:25:16 -0700 Subject: [PATCH 12/12] use != --- kindlecomicconverter/KCC_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 82d3bf3..b5ef4b6 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -1825,7 +1825,7 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): if not volumes: return None, 'No valid volume numbers parsed' if len(volumes) != fileCount: - return None, f'Volume count ({len(volumes)}) ≠ file count ({fileCount})' + return None, f'Volume count ({len(volumes)}) != file count ({fileCount})' return volumes, None