""" Rule configuration widgets: each rule type has a small form that updates the Rule model. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QComboBox, QCheckBox, QGroupBox, QFormLayout, QStackedWidget, QPushButton, QFileDialog, ) from PyQt6.QtCore import pyqtSignal from engine.rules import ( ReplaceRule, InsertRule, RemoveRule, CaseRule, NumberingRule, EpisodeRenumberRule, RegexRule, PrefixSuffixRule, CsvMappingRule, ) class ReplaceRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.find = QLineEdit() self.find.setPlaceholderText("Text to find") self.find.textChanged.connect(self._emit) self.replace = QLineEdit() self.replace.setPlaceholderText("Replace with") self.replace.textChanged.connect(self._emit) self.caseSensitive = QCheckBox("Case sensitive") self.caseSensitive.toggled.connect(self._emit) self.wholeWord = QCheckBox("Whole word only") self.wholeWord.toggled.connect(self._emit) layout.addRow("Find:", self.find) layout.addRow("Replace with:", self.replace) layout.addRow(self.caseSensitive) layout.addRow(self.wholeWord) def _emit(self): self.ruleChanged.emit() def getRule(self) -> ReplaceRule: r = ReplaceRule( find=self.find.text(), replace=self.replace.text(), case_sensitive=self.caseSensitive.isChecked(), whole_word=self.wholeWord.isChecked(), ) r.enabled = self.enabled_cb.isChecked() return r class RegexRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.pattern = QLineEdit() self.pattern.setPlaceholderText(r"e.g. S(\d+)E(\d+)") self.pattern.textChanged.connect(self._emit) self.replacement = QLineEdit() self.replacement.setPlaceholderText(r"e.g. S\1E\2") self.replacement.textChanged.connect(self._emit) layout.addRow("Regex pattern:", self.pattern) layout.addRow("Replacement:", self.replacement) def _emit(self): self.ruleChanged.emit() def getRule(self) -> RegexRule: r = RegexRule(pattern=self.pattern.text(), replacement=self.replacement.text()) r.enabled = self.enabled_cb.isChecked() return r class InsertRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.text = QLineEdit() self.text.setPlaceholderText("Text to insert") self.text.textChanged.connect(self._emit) self.position = QComboBox() self.position.addItems(["At start", "At end", "At position..."]) self.position.setCurrentIndex(0) self.position.currentIndexChanged.connect(self._emit) self.positionSpin = QSpinBox() self.positionSpin.setMinimum(0) self.positionSpin.setMaximum(9999) self.positionSpin.valueChanged.connect(self._emit) layout.addRow("Text:", self.text) layout.addRow("Position:", self.position) layout.addRow("Index:", self.positionSpin) def _emit(self): self.ruleChanged.emit() def getRule(self) -> InsertRule: idx = self.position.currentIndex() if idx == 0: pos = 0 elif idx == 1: pos = -1 else: pos = self.positionSpin.value() r = InsertRule(text=self.text.text(), position=pos) r.enabled = self.enabled_cb.isChecked() return r class RemoveRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.removeType = QComboBox() self.removeType.addItems([ "Remove text", "Remove all digits", "Remove first N characters", "Remove last N characters", ]) self.removeType.currentIndexChanged.connect(self._emit) self.value = QLineEdit() self.value.setPlaceholderText("Text or number") self.value.textChanged.connect(self._emit) layout.addRow("Type:", self.removeType) layout.addRow("Value:", self.value) def _emit(self): self.ruleChanged.emit() def getRule(self) -> RemoveRule: idx = self.removeType.currentIndex() types = ["chars", "digits", "first_n", "last_n"] r = RemoveRule(remove_type=types[idx], value=self.value.text()) r.enabled = self.enabled_cb.isChecked() return r class CaseRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.caseType = QComboBox() self.caseType.addItems(["Title Case", "UPPER", "lower", "Sentence case"]) self.caseType.currentIndexChanged.connect(self._emit) layout.addRow("Case:", self.caseType) def _emit(self): self.ruleChanged.emit() def getRule(self) -> CaseRule: types = ["title", "upper", "lower", "sentence"] r = CaseRule(case_type=types[self.caseType.currentIndex()]) r.enabled = self.enabled_cb.isChecked() return r class NumberingRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.start = QSpinBox() self.start.setMinimum(-9999) self.start.setMaximum(99999) self.start.setValue(1) self.start.valueChanged.connect(self._emit) self.step = QSpinBox() self.step.setMinimum(1) self.step.setMaximum(100) self.step.setValue(1) self.step.valueChanged.connect(self._emit) self.padding = QSpinBox() self.padding.setMinimum(1) self.padding.setMaximum(10) self.padding.setValue(2) self.padding.valueChanged.connect(self._emit) self.where = QComboBox() self.where.addItems(["Prefix", "Suffix", "Insert at index"]) self.where.currentIndexChanged.connect(self._emit) self.separator = QLineEdit() self.separator.setText(" ") self.separator.textChanged.connect(self._emit) self.insertAt = QSpinBox() self.insertAt.setMinimum(0) self.insertAt.setMaximum(9999) self.insertAt.valueChanged.connect(self._emit) layout.addRow("Start:", self.start) layout.addRow("Step:", self.step) layout.addRow("Padding:", self.padding) layout.addRow("Where:", self.where) layout.addRow("Separator:", self.separator) layout.addRow("Insert at index:", self.insertAt) def _emit(self): self.ruleChanged.emit() def getRule(self) -> NumberingRule: where_map = ["prefix", "suffix", "insert_at"] r = NumberingRule( start=self.start.value(), step=self.step.value(), padding=self.padding.value(), where=where_map[self.where.currentIndex()], insert_at=self.insertAt.value(), separator=self.separator.text() or " ", ) r.enabled = self.enabled_cb.isChecked() return r class EpisodeRenumberRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.start = QSpinBox() self.start.setMinimum(1) self.start.setMaximum(9999) self.start.setValue(1) self.start.valueChanged.connect(self._emit) self.step = QSpinBox() self.step.setMinimum(1) self.step.setValue(1) self.step.valueChanged.connect(self._emit) self.padding = QSpinBox() self.padding.setMinimum(1) self.padding.setMaximum(3) self.padding.setValue(2) self.padding.valueChanged.connect(self._emit) layout.addRow("First episode number:", self.start) layout.addRow("Step:", self.step) layout.addRow("Zero-pad width:", self.padding) info = QLabel("Matches patterns like S01E05 - Title or Show 1x03 - Title. Episode number is replaced; title is kept.") info.setWordWrap(True) layout.addRow(info) def _emit(self): self.ruleChanged.emit() def getRule(self) -> EpisodeRenumberRule: r = EpisodeRenumberRule( start=self.start.value(), step=self.step.value(), padding=self.padding.value(), ) r.enabled = self.enabled_cb.isChecked() return r class PrefixSuffixRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) self.prefix = QLineEdit() self.prefix.setPlaceholderText("Prefix") self.prefix.textChanged.connect(self._emit) self.suffix = QLineEdit() self.suffix.setPlaceholderText("Suffix") self.suffix.textChanged.connect(self._emit) layout.addRow("Prefix:", self.prefix) layout.addRow("Suffix:", self.suffix) def _emit(self): self.ruleChanged.emit() def getRule(self) -> PrefixSuffixRule: r = PrefixSuffixRule(prefix=self.prefix.text(), suffix=self.suffix.text()) r.enabled = self.enabled_cb.isChecked() return r class CsvMappingRuleWidget(QWidget): ruleChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) layout = QFormLayout(self) self.enabled_cb = QCheckBox("Use this rule") self.enabled_cb.setChecked(False) self.enabled_cb.toggled.connect(self._emit) layout.addRow(self.enabled_cb) path_row = QHBoxLayout() self.path_edit = QLineEdit() self.path_edit.setPlaceholderText("Path to CSV file…") self.path_edit.textChanged.connect(self._emit) browse_btn = QPushButton("Browse…") browse_btn.clicked.connect(self._browse) path_row.addWidget(self.path_edit, 1) path_row.addWidget(browse_btn) layout.addRow("CSV file:", path_row) info = QLabel('CSV must have columns "Original Name" and "Target Name". Lookup is by current filename.') info.setWordWrap(True) layout.addRow(info) def _browse(self): path, _ = QFileDialog.getOpenFileName( self, "Select CSV", "", "CSV (*.csv);;All files (*)", ) if path: self.path_edit.setText(path) def _emit(self): self.ruleChanged.emit() def getRule(self) -> CsvMappingRule: r = CsvMappingRule(csv_path=self.path_edit.text().strip()) r.enabled = self.enabled_cb.isChecked() return r