mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
808 lines
33 KiB
Python
808 lines
33 KiB
Python
# a simple window with a list of items in the queue, no checkboxes
|
|
# button to remove an item from the queue
|
|
# button to clear the queue
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QDialogButtonBox,
|
|
QPushButton,
|
|
QListWidget,
|
|
QListWidgetItem,
|
|
QFileIconProvider,
|
|
QLabel,
|
|
QWidget,
|
|
QSizePolicy,
|
|
QAbstractItemView,
|
|
)
|
|
from PyQt6.QtCore import QFileInfo, Qt
|
|
from abogen.constants import COLORS
|
|
from copy import deepcopy
|
|
from PyQt6.QtGui import QFontMetrics
|
|
|
|
|
|
class ElidedLabel(QLabel):
|
|
def __init__(self, text):
|
|
super().__init__(text)
|
|
self._full_text = text
|
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
self.setTextFormat(Qt.TextFormat.PlainText)
|
|
|
|
def setText(self, text):
|
|
self._full_text = text
|
|
super().setText(text)
|
|
self.update()
|
|
|
|
def resizeEvent(self, event):
|
|
metrics = QFontMetrics(self.font())
|
|
elided = metrics.elidedText(
|
|
self._full_text, Qt.TextElideMode.ElideRight, self.width()
|
|
)
|
|
super().setText(elided)
|
|
super().resizeEvent(event)
|
|
|
|
def fullText(self):
|
|
return self._full_text
|
|
|
|
|
|
class QueueListItemWidget(QWidget):
|
|
def __init__(self, file_name, char_count):
|
|
super().__init__()
|
|
layout = QHBoxLayout()
|
|
layout.setContentsMargins(12, 0, 6, 0)
|
|
layout.setSpacing(0)
|
|
import os
|
|
|
|
name_label = ElidedLabel(os.path.basename(file_name))
|
|
char_label = QLabel(f"Chars: {char_count}")
|
|
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
|
|
char_label.setAlignment(
|
|
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
|
)
|
|
char_label.setSizePolicy(
|
|
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred
|
|
)
|
|
layout.addWidget(name_label, 1)
|
|
layout.addWidget(char_label, 0)
|
|
self.setLayout(layout)
|
|
|
|
|
|
class DroppableQueueListWidget(QListWidget):
|
|
def __init__(self, parent_dialog):
|
|
super().__init__()
|
|
self.parent_dialog = parent_dialog
|
|
self.setAcceptDrops(True)
|
|
# Overlay for drag hover
|
|
self.drag_overlay = QLabel("", self)
|
|
self.drag_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.drag_overlay.setStyleSheet(
|
|
f"border:2px dashed {COLORS['BLUE_BORDER_HOVER']}; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG_HOVER']};"
|
|
)
|
|
self.drag_overlay.setVisible(False)
|
|
self.drag_overlay.setAttribute(
|
|
Qt.WidgetAttribute.WA_TransparentForMouseEvents, True
|
|
)
|
|
|
|
def dragEnterEvent(self, event):
|
|
if event.mimeData().hasUrls():
|
|
for url in event.mimeData().urls():
|
|
file_path = url.toLocalFile().lower()
|
|
if url.isLocalFile() and (
|
|
file_path.endswith(".txt")
|
|
or file_path.endswith((".srt", ".ass", ".vtt"))
|
|
):
|
|
self.drag_overlay.resize(self.size())
|
|
self.drag_overlay.setVisible(True)
|
|
event.acceptProposedAction()
|
|
return
|
|
self.drag_overlay.setVisible(False)
|
|
event.ignore()
|
|
|
|
def dragMoveEvent(self, event):
|
|
if event.mimeData().hasUrls():
|
|
for url in event.mimeData().urls():
|
|
file_path = url.toLocalFile().lower()
|
|
if url.isLocalFile() and (
|
|
file_path.endswith(".txt")
|
|
or file_path.endswith((".srt", ".ass", ".vtt"))
|
|
):
|
|
event.acceptProposedAction()
|
|
return
|
|
event.ignore()
|
|
|
|
def dragLeaveEvent(self, event):
|
|
self.drag_overlay.setVisible(False)
|
|
event.accept()
|
|
|
|
def dropEvent(self, event):
|
|
self.drag_overlay.setVisible(False)
|
|
if event.mimeData().hasUrls():
|
|
file_paths = [
|
|
url.toLocalFile()
|
|
for url in event.mimeData().urls()
|
|
if url.isLocalFile()
|
|
and (
|
|
url.toLocalFile().lower().endswith(".txt")
|
|
or url.toLocalFile().lower().endswith((".srt", ".ass", ".vtt"))
|
|
)
|
|
]
|
|
if file_paths:
|
|
self.parent_dialog.add_files_from_paths(file_paths)
|
|
event.acceptProposedAction()
|
|
else:
|
|
event.ignore()
|
|
else:
|
|
event.ignore()
|
|
|
|
def resizeEvent(self, event):
|
|
super().resizeEvent(event)
|
|
if hasattr(self, "drag_overlay"):
|
|
self.drag_overlay.resize(self.size())
|
|
|
|
|
|
class QueueManager(QDialog):
|
|
def __init__(self, parent, queue: list, title="Queue Manager", size=(600, 700)):
|
|
super().__init__()
|
|
self.queue = queue
|
|
self._original_queue = deepcopy(
|
|
queue
|
|
) # Store a deep copy of the original queue
|
|
self.parent = parent
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15) # set main layout margins
|
|
layout.setSpacing(12) # set spacing between widgets in main layout
|
|
# list of queued items
|
|
self.listwidget = DroppableQueueListWidget(self)
|
|
self.listwidget.setSelectionMode(
|
|
QAbstractItemView.SelectionMode.ExtendedSelection
|
|
)
|
|
self.listwidget.setAlternatingRowColors(True)
|
|
self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
|
|
# Add informative instructions at the top
|
|
instructions = QLabel(
|
|
"<h2>How Queue Works?</h2>"
|
|
"You can add text and subtitle files (.txt, .srt, .ass, .vtt) directly using the '<b>Add files</b>' button below. "
|
|
"To add PDF, EPUB or markdown files, use the input box in the main window and click the <b>'Add to Queue'</b> button. "
|
|
"Each file in the queue keeps the configuration settings active when it was added. "
|
|
"Changing the main window configuration afterward <b>does not</b> affect files already in the queue. "
|
|
"You can view each file's configuration by hovering over them."
|
|
)
|
|
instructions.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
|
instructions.setWordWrap(True)
|
|
instructions.setStyleSheet("margin-bottom: 8px;")
|
|
layout.addWidget(instructions)
|
|
# Overlay label for empty queue
|
|
self.empty_overlay = QLabel(
|
|
"Drag and drop your text or subtitle files here or use the 'Add files' button.",
|
|
self.listwidget,
|
|
)
|
|
self.empty_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.empty_overlay.setStyleSheet(
|
|
f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
|
|
)
|
|
self.empty_overlay.setWordWrap(True)
|
|
self.empty_overlay.setAttribute(
|
|
Qt.WidgetAttribute.WA_TransparentForMouseEvents, True
|
|
)
|
|
self.empty_overlay.hide()
|
|
# add queue items to the list
|
|
self.process_queue()
|
|
|
|
button_row = QHBoxLayout()
|
|
button_row.setContentsMargins(0, 0, 0, 0) # optional: no margins for button row
|
|
button_row.setSpacing(7) # set spacing between buttons
|
|
# Add files button
|
|
add_files_button = QPushButton("Add files")
|
|
add_files_button.setFixedHeight(40)
|
|
add_files_button.clicked.connect(self.add_more_files)
|
|
button_row.addWidget(add_files_button)
|
|
|
|
# Remove button
|
|
self.remove_button = QPushButton("Remove selected")
|
|
self.remove_button.setFixedHeight(40)
|
|
self.remove_button.clicked.connect(self.remove_item)
|
|
button_row.addWidget(self.remove_button)
|
|
|
|
# Clear button
|
|
self.clear_button = QPushButton("Clear Queue")
|
|
self.clear_button.setFixedHeight(40)
|
|
self.clear_button.clicked.connect(self.clear_queue)
|
|
button_row.addWidget(self.clear_button)
|
|
|
|
layout.addLayout(button_row)
|
|
layout.addWidget(self.listwidget)
|
|
|
|
# Connect selection change to update button state
|
|
self.listwidget.currentItemChanged.connect(self.update_button_states)
|
|
self.listwidget.itemSelectionChanged.connect(self.update_button_states)
|
|
|
|
buttons = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
|
self,
|
|
)
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
|
|
layout.addWidget(buttons)
|
|
|
|
self.setLayout(layout)
|
|
|
|
self.setWindowTitle(title)
|
|
self.resize(*size)
|
|
|
|
self.update_button_states()
|
|
|
|
def process_queue(self):
|
|
"""Process the queue items."""
|
|
import os
|
|
|
|
self.listwidget.clear()
|
|
if not self.queue:
|
|
self.empty_overlay.show()
|
|
self.update_button_states()
|
|
return
|
|
else:
|
|
self.empty_overlay.hide()
|
|
icon_provider = QFileIconProvider()
|
|
for item in self.queue:
|
|
# Determine display file path (prefer save_base_path for original file)
|
|
display_file_path = getattr(item, "save_base_path", None) or item.file_name
|
|
processing_file_path = item.file_name
|
|
|
|
# Normalize paths for consistent display (fixes Windows path separator issues)
|
|
display_file_path = (
|
|
os.path.normpath(display_file_path)
|
|
if display_file_path
|
|
else display_file_path
|
|
)
|
|
processing_file_path = (
|
|
os.path.normpath(processing_file_path)
|
|
if processing_file_path
|
|
else processing_file_path
|
|
)
|
|
|
|
# Only show the file name, not the full path
|
|
display_name = display_file_path
|
|
|
|
if os.path.sep in display_file_path:
|
|
display_name = os.path.basename(display_file_path)
|
|
# Get icon for the display file
|
|
icon = icon_provider.icon(QFileInfo(display_file_path))
|
|
list_item = QListWidgetItem()
|
|
# Set tooltip with detailed info
|
|
output_folder = getattr(item, "output_folder", "")
|
|
# For plain .txt inputs we don't need to show a separate processing file
|
|
show_processing = True
|
|
try:
|
|
if isinstance(
|
|
display_file_path, str
|
|
) and display_file_path.lower().endswith(".txt"):
|
|
show_processing = False
|
|
except Exception:
|
|
show_processing = True
|
|
|
|
tooltip = f"<b>Input File:</b> {display_file_path}<br>"
|
|
if (
|
|
show_processing
|
|
and processing_file_path
|
|
and processing_file_path != display_file_path
|
|
):
|
|
tooltip += f"<b>Processing File:</b> {processing_file_path}<br>"
|
|
tooltip += (
|
|
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
|
|
f"<b>Speed:</b> {getattr(item, 'speed', '')}<br>"
|
|
f"<b>Voice:</b> {getattr(item, 'voice', '')}<br>"
|
|
f"<b>Save Option:</b> {getattr(item, 'save_option', '')}<br>"
|
|
)
|
|
if output_folder not in (None, "", "None"):
|
|
tooltip += f"<b>Output Folder:</b> {output_folder}<br>"
|
|
tooltip += (
|
|
f"<b>Subtitle Mode:</b> {getattr(item, 'subtitle_mode', '')}<br>"
|
|
f"<b>Output Format:</b> {getattr(item, 'output_format', '')}<br>"
|
|
f"<b>Characters:</b> {getattr(item, 'total_char_count', '')}<br>"
|
|
f"<b>Replace Single Newlines:</b> {getattr(item, 'replace_single_newlines', False)}<br>"
|
|
f"<b>Use Silent Gaps:</b> {getattr(item, 'use_silent_gaps', False)}<br>"
|
|
f"<b>Speed Method:</b> {getattr(item, 'subtitle_speed_method', 'tts')}"
|
|
)
|
|
# Add book handler options if present
|
|
save_chapters_separately = getattr(item, "save_chapters_separately", None)
|
|
merge_chapters_at_end = getattr(item, "merge_chapters_at_end", None)
|
|
if save_chapters_separately is not None:
|
|
tooltip += f"<br><b>Save chapters separately:</b> {'Yes' if save_chapters_separately else 'No'}"
|
|
# Only show merge option if saving chapters separately
|
|
if save_chapters_separately and merge_chapters_at_end is not None:
|
|
tooltip += f"<br><b>Merge chapters at the end:</b> {'Yes' if merge_chapters_at_end else 'No'}"
|
|
list_item.setToolTip(tooltip)
|
|
list_item.setIcon(icon)
|
|
# Store both paths for context menu
|
|
list_item.setData(
|
|
Qt.ItemDataRole.UserRole,
|
|
{
|
|
"display_path": display_file_path,
|
|
"processing_path": processing_file_path,
|
|
},
|
|
)
|
|
# Use custom widget for display
|
|
char_count = getattr(item, "total_char_count", 0)
|
|
widget = QueueListItemWidget(display_file_path, char_count)
|
|
self.listwidget.addItem(list_item)
|
|
self.listwidget.setItemWidget(list_item, widget)
|
|
self.update_button_states()
|
|
|
|
def remove_item(self):
|
|
items = self.listwidget.selectedItems()
|
|
if not items:
|
|
return
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
# Remove by index to ensure correct mapping
|
|
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
|
|
# Warn user if removing multiple files
|
|
if len(rows) > 1:
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Remove",
|
|
f"Are you sure you want to remove {len(rows)} selected items from the queue?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
for row in rows:
|
|
if 0 <= row < len(self.queue):
|
|
del self.queue[row]
|
|
self.process_queue()
|
|
self.update_button_states()
|
|
|
|
def clear_queue(self):
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
if len(self.queue) > 1:
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Clear Queue",
|
|
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
self.queue.clear()
|
|
self.listwidget.clear()
|
|
self.empty_overlay.resize(
|
|
self.listwidget.size()
|
|
) # Ensure overlay is sized correctly
|
|
self.empty_overlay.show() # Show the overlay when queue is empty
|
|
self.update_button_states()
|
|
|
|
def get_queue(self):
|
|
return self.queue
|
|
|
|
def get_current_attributes(self):
|
|
# Fetch current attribute values from the parent abogen GUI
|
|
attrs = {}
|
|
parent = self.parent
|
|
if parent is not None:
|
|
# lang_code: use parent's get_voice_formula and get_selected_lang
|
|
if hasattr(parent, "get_voice_formula") and hasattr(
|
|
parent, "get_selected_lang"
|
|
):
|
|
voice_formula = parent.get_voice_formula()
|
|
attrs["lang_code"] = parent.get_selected_lang(voice_formula)
|
|
attrs["voice"] = voice_formula
|
|
else:
|
|
attrs["lang_code"] = getattr(parent, "selected_lang", "")
|
|
attrs["voice"] = getattr(parent, "selected_voice", "")
|
|
# speed
|
|
if hasattr(parent, "speed_slider"):
|
|
attrs["speed"] = parent.speed_slider.value() / 100.0
|
|
else:
|
|
attrs["speed"] = getattr(parent, "speed", 1.0)
|
|
# save_option
|
|
attrs["save_option"] = getattr(parent, "save_option", "")
|
|
# output_folder
|
|
attrs["output_folder"] = getattr(parent, "selected_output_folder", "")
|
|
# subtitle_mode
|
|
if hasattr(parent, "get_actual_subtitle_mode"):
|
|
attrs["subtitle_mode"] = parent.get_actual_subtitle_mode()
|
|
else:
|
|
attrs["subtitle_mode"] = getattr(parent, "subtitle_mode", "")
|
|
# output_format
|
|
attrs["output_format"] = getattr(parent, "selected_format", "")
|
|
# total_char_count
|
|
attrs["total_char_count"] = getattr(parent, "char_count", "")
|
|
# replace_single_newlines
|
|
attrs["replace_single_newlines"] = getattr(
|
|
parent, "replace_single_newlines", False
|
|
)
|
|
# use_silent_gaps
|
|
attrs["use_silent_gaps"] = getattr(parent, "use_silent_gaps", False)
|
|
# subtitle_speed_method
|
|
attrs["subtitle_speed_method"] = getattr(
|
|
parent, "subtitle_speed_method", "tts"
|
|
)
|
|
# book handler options
|
|
attrs["save_chapters_separately"] = getattr(
|
|
parent, "save_chapters_separately", None
|
|
)
|
|
attrs["merge_chapters_at_end"] = getattr(
|
|
parent, "merge_chapters_at_end", None
|
|
)
|
|
else:
|
|
# fallback: empty values
|
|
attrs = {
|
|
k: ""
|
|
for k in [
|
|
"lang_code",
|
|
"speed",
|
|
"voice",
|
|
"save_option",
|
|
"output_folder",
|
|
"subtitle_mode",
|
|
"output_format",
|
|
"total_char_count",
|
|
"replace_single_newlines",
|
|
]
|
|
}
|
|
attrs["save_chapters_separately"] = None
|
|
attrs["merge_chapters_at_end"] = None
|
|
return attrs
|
|
|
|
def add_files_from_paths(self, file_paths):
|
|
from abogen.utils import calculate_text_length
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
import os
|
|
|
|
current_attrs = self.get_current_attributes()
|
|
duplicates = []
|
|
for file_path in file_paths:
|
|
|
|
class QueueItem:
|
|
pass
|
|
|
|
item = QueueItem()
|
|
item.file_name = file_path
|
|
item.save_base_path = (
|
|
file_path # For .txt files, processing and save paths are the same
|
|
)
|
|
for attr, value in current_attrs.items():
|
|
setattr(item, attr, value)
|
|
# Override subtitle_mode to "Disabled" for subtitle files
|
|
if file_path.lower().endswith((".srt", ".ass", ".vtt")):
|
|
item.subtitle_mode = "Disabled"
|
|
# Read file content and calculate total_char_count using calculate_text_length
|
|
try:
|
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
file_content = f.read()
|
|
item.total_char_count = calculate_text_length(file_content)
|
|
except Exception:
|
|
item.total_char_count = 0
|
|
# Prevent adding duplicate items to the queue (check all attributes)
|
|
is_duplicate = False
|
|
for queued_item in self.queue:
|
|
if (
|
|
getattr(queued_item, "file_name", None)
|
|
== getattr(item, "file_name", None)
|
|
and getattr(queued_item, "lang_code", None)
|
|
== getattr(item, "lang_code", None)
|
|
and getattr(queued_item, "speed", None)
|
|
== getattr(item, "speed", None)
|
|
and getattr(queued_item, "voice", None)
|
|
== getattr(item, "voice", None)
|
|
and getattr(queued_item, "save_option", None)
|
|
== getattr(item, "save_option", None)
|
|
and getattr(queued_item, "output_folder", None)
|
|
== getattr(item, "output_folder", None)
|
|
and getattr(queued_item, "subtitle_mode", None)
|
|
== getattr(item, "subtitle_mode", None)
|
|
and getattr(queued_item, "output_format", None)
|
|
== getattr(item, "output_format", None)
|
|
and getattr(queued_item, "total_char_count", None)
|
|
== getattr(item, "total_char_count", None)
|
|
and getattr(queued_item, "replace_single_newlines", False)
|
|
== getattr(item, "replace_single_newlines", False)
|
|
and getattr(queued_item, "use_silent_gaps", False)
|
|
== getattr(item, "use_silent_gaps", False)
|
|
and getattr(queued_item, "subtitle_speed_method", "tts")
|
|
== getattr(item, "subtitle_speed_method", "tts")
|
|
and getattr(queued_item, "save_base_path", None)
|
|
== getattr(item, "save_base_path", None)
|
|
and getattr(queued_item, "save_chapters_separately", None)
|
|
== getattr(item, "save_chapters_separately", None)
|
|
and getattr(queued_item, "merge_chapters_at_end", None)
|
|
== getattr(item, "merge_chapters_at_end", None)
|
|
):
|
|
is_duplicate = True
|
|
break
|
|
if is_duplicate:
|
|
duplicates.append(os.path.basename(file_path))
|
|
continue
|
|
self.queue.append(item)
|
|
if duplicates:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Duplicate Item(s)",
|
|
f"Skipping {len(duplicates)} file(s) with the same attributes, already in the queue.",
|
|
)
|
|
self.process_queue()
|
|
self.update_button_states()
|
|
|
|
def add_more_files(self):
|
|
from PyQt6.QtWidgets import QFileDialog
|
|
from abogen.utils import calculate_text_length # import the function
|
|
|
|
# Allow .txt, .srt, .ass, and .vtt files
|
|
files, _ = QFileDialog.getOpenFileNames(
|
|
self,
|
|
"Select text or subtitle files",
|
|
"",
|
|
"Supported Files (*.txt *.srt *.ass *.vtt)",
|
|
)
|
|
if not files:
|
|
return
|
|
self.add_files_from_paths(files)
|
|
|
|
def resizeEvent(self, event):
|
|
super().resizeEvent(event)
|
|
if hasattr(self, "empty_overlay"):
|
|
self.empty_overlay.resize(self.listwidget.size())
|
|
|
|
def update_button_states(self):
|
|
# Enable Remove if at least one item is selected, else disable
|
|
if hasattr(self, "remove_button"):
|
|
selected_count = len(self.listwidget.selectedItems())
|
|
self.remove_button.setEnabled(selected_count > 0)
|
|
if selected_count > 1:
|
|
self.remove_button.setText(f"Remove selected ({selected_count})")
|
|
else:
|
|
self.remove_button.setText("Remove selected")
|
|
# Disable Clear if queue is empty
|
|
if hasattr(self, "clear_button"):
|
|
self.clear_button.setEnabled(bool(self.queue))
|
|
|
|
def show_context_menu(self, pos):
|
|
from PyQt6.QtWidgets import QMenu
|
|
from PyQt6.QtGui import QAction, QDesktopServices
|
|
from PyQt6.QtCore import QUrl
|
|
import os
|
|
|
|
global_pos = self.listwidget.viewport().mapToGlobal(pos)
|
|
selected_items = self.listwidget.selectedItems()
|
|
menu = QMenu(self)
|
|
if len(selected_items) == 1:
|
|
# Add Remove action
|
|
remove_action = QAction("Remove this item", self)
|
|
remove_action.triggered.connect(self.remove_item)
|
|
menu.addAction(remove_action)
|
|
|
|
# Get paths for determining if it's a document input
|
|
item = selected_items[0]
|
|
paths = item.data(Qt.ItemDataRole.UserRole)
|
|
if isinstance(paths, dict):
|
|
display_path = paths.get("display_path", "")
|
|
processing_path = paths.get("processing_path", "")
|
|
else:
|
|
display_path = paths
|
|
processing_path = paths
|
|
|
|
doc_exts = (".md", ".markdown", ".pdf", ".epub")
|
|
is_document_input = (
|
|
isinstance(display_path, str)
|
|
and display_path.lower().endswith(doc_exts)
|
|
) or (
|
|
isinstance(processing_path, str)
|
|
and processing_path.lower().endswith(doc_exts)
|
|
)
|
|
|
|
# Add Open file action(s)
|
|
def open_file_by_path(path_label: str):
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
p = display_path if path_label == "display" else processing_path
|
|
if not p:
|
|
QMessageBox.warning(
|
|
self, "File Not Found", "Path is not available."
|
|
)
|
|
return
|
|
|
|
# Find the queue item and resolve the target path
|
|
target_path = None
|
|
for q in self.queue:
|
|
if (
|
|
getattr(q, "save_base_path", None) == display_path
|
|
or q.file_name == display_path
|
|
):
|
|
if path_label == "display":
|
|
target_path = (
|
|
getattr(q, "save_base_path", None) or q.file_name
|
|
)
|
|
else:
|
|
target_path = q.file_name
|
|
break
|
|
if (
|
|
getattr(q, "save_base_path", None) == processing_path
|
|
or q.file_name == processing_path
|
|
):
|
|
if path_label == "display":
|
|
target_path = (
|
|
getattr(q, "save_base_path", None) or q.file_name
|
|
)
|
|
else:
|
|
target_path = q.file_name
|
|
break
|
|
|
|
# Fallback to the raw path if resolution failed
|
|
if not target_path:
|
|
target_path = p
|
|
|
|
if not os.path.exists(target_path):
|
|
QMessageBox.warning(
|
|
self, "File Not Found", f"The file does not exist."
|
|
)
|
|
return
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(target_path))
|
|
|
|
if is_document_input:
|
|
# For documents, show two open options
|
|
open_processed_action = QAction("Open processed file", self)
|
|
open_processed_action.triggered.connect(
|
|
lambda: open_file_by_path("processing")
|
|
)
|
|
menu.addAction(open_processed_action)
|
|
|
|
open_input_action = QAction("Open input file", self)
|
|
open_input_action.triggered.connect(
|
|
lambda: open_file_by_path("display")
|
|
)
|
|
menu.addAction(open_input_action)
|
|
else:
|
|
# For plain text files, show single open option
|
|
open_file_action = QAction("Open file", self)
|
|
open_file_action.triggered.connect(lambda: open_file_by_path("display"))
|
|
menu.addAction(open_file_action)
|
|
|
|
# Add Go to folder action
|
|
# If the queued item represents a converted document (markdown, pdf, epub)
|
|
# show two actions: Go to processed file (the cached .txt) and Go to input file (original source)
|
|
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
def open_folder_for(path_label: str):
|
|
# path_label should be either 'display' or 'processing'
|
|
p = display_path if path_label == "display" else processing_path
|
|
if not p:
|
|
QMessageBox.warning(
|
|
self, "File Not Found", "Path is not available."
|
|
)
|
|
return
|
|
# If the stored path is the display path (original) but the actual file may be
|
|
# stored on the queue object differently, try to resolve via the queue entry.
|
|
target_path = None
|
|
for q in self.queue:
|
|
if (
|
|
getattr(q, "save_base_path", None) == display_path
|
|
or q.file_name == display_path
|
|
):
|
|
if path_label == "display":
|
|
target_path = (
|
|
getattr(q, "save_base_path", None) or q.file_name
|
|
)
|
|
else:
|
|
target_path = q.file_name
|
|
break
|
|
if (
|
|
getattr(q, "save_base_path", None) == processing_path
|
|
or q.file_name == processing_path
|
|
):
|
|
if path_label == "display":
|
|
target_path = (
|
|
getattr(q, "save_base_path", None) or q.file_name
|
|
)
|
|
else:
|
|
target_path = q.file_name
|
|
break
|
|
# Fallback to the raw path if resolution failed
|
|
if not target_path:
|
|
target_path = p
|
|
|
|
if not os.path.exists(target_path):
|
|
QMessageBox.warning(
|
|
self,
|
|
"File Not Found",
|
|
f"The file does not exist: {target_path}",
|
|
)
|
|
return
|
|
folder = os.path.dirname(target_path)
|
|
if os.path.exists(folder):
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder))
|
|
|
|
if is_document_input:
|
|
processed_action = QAction("Go to processed file", self)
|
|
processed_action.triggered.connect(
|
|
lambda: open_folder_for("processing")
|
|
)
|
|
menu.addAction(processed_action)
|
|
|
|
input_action = QAction("Go to input file", self)
|
|
input_action.triggered.connect(lambda: open_folder_for("display"))
|
|
menu.addAction(input_action)
|
|
else:
|
|
# Default behavior for non-document inputs: single "Go to folder" action
|
|
go_to_folder_action = QAction("Go to folder", self)
|
|
|
|
def go_to_folder():
|
|
item = selected_items[0]
|
|
paths = item.data(Qt.ItemDataRole.UserRole)
|
|
if isinstance(paths, dict):
|
|
file_path = paths.get(
|
|
"display_path", paths.get("processing_path", "")
|
|
)
|
|
else:
|
|
file_path = paths # Fallback for old format
|
|
# Find the queue item
|
|
for q in self.queue:
|
|
if (
|
|
getattr(q, "save_base_path", None) == file_path
|
|
or q.file_name == file_path
|
|
):
|
|
target_path = (
|
|
getattr(q, "save_base_path", None) or q.file_name
|
|
)
|
|
if not os.path.exists(target_path):
|
|
QMessageBox.warning(
|
|
self, "File Not Found", f"The file does not exist."
|
|
)
|
|
return
|
|
folder = os.path.dirname(target_path)
|
|
if os.path.exists(folder):
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder))
|
|
break
|
|
|
|
go_to_folder_action.triggered.connect(go_to_folder)
|
|
menu.addAction(go_to_folder_action)
|
|
|
|
elif len(selected_items) > 1:
|
|
remove_action = QAction(f"Remove selected ({len(selected_items)})", self)
|
|
remove_action.triggered.connect(self.remove_item)
|
|
menu.addAction(remove_action)
|
|
# Always add Clear Queue
|
|
clear_action = QAction("Clear Queue", self)
|
|
clear_action.triggered.connect(self.clear_queue)
|
|
menu.addAction(clear_action)
|
|
menu.exec(global_pos)
|
|
|
|
def accept(self):
|
|
# Accept: keep changes
|
|
super().accept()
|
|
|
|
def reject(self):
|
|
# Cancel: restore original queue
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
|
|
# Warn if user changed a lot (e.g., more than 1 items difference)
|
|
original_count = len(self._original_queue)
|
|
current_count = len(self.queue)
|
|
if abs(original_count - current_count) > 1:
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Cancel",
|
|
f"Are you sure you want to cancel and discard all changes?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
self.queue.clear()
|
|
self.queue.extend(deepcopy(self._original_queue))
|
|
super().reject()
|
|
|
|
def keyPressEvent(self, event):
|
|
from PyQt6.QtCore import Qt
|
|
|
|
if event.key() == Qt.Key.Key_Delete:
|
|
self.remove_item()
|
|
else:
|
|
super().keyPressEvent(event)
|