Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Install Qt runtime libraries (headless)
run: >
sudo apt-get update && sudo apt-get install -y --no-install-recommends
libegl1 libgl1 libxkbcommon0 libdbus-1-3 libfontconfig1
libegl1 libgl1 libxkbcommon0 libdbus-1-3 libfontconfig1 libpulse0

- name: Install uv
uses: astral-sh/setup-uv@v5
Expand Down
7 changes: 7 additions & 0 deletions src/scripto/gui/viewmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ def history_clean_missing(self) -> int:
stale = {e.id for e, exists in self.history_rows() if not exists}
return self.history.remove(stale) if stale else 0

def history_delete_sources(self, sources: set[str]) -> int:
"""Remove every history entry for the given source files.

Records only — output files on disk are never touched."""
ids = {e.id for e in self.history.entries() if e.source in sources}
return self.history.remove(ids) if ids else 0

@staticmethod
def read_preview(path: str, limit: int = 20_000) -> str:
text = Path(path).read_text(encoding="utf-8", errors="replace")
Expand Down
142 changes: 141 additions & 1 deletion src/scripto/gui_qt/history_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from pathlib import Path

from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QScrollArea,
QTextBrowser,
Expand All @@ -33,15 +35,21 @@ def __init__(self, window):

def _build(self) -> None:
t = self.t
self._selected: set[str] = set()
refresh_btn = QPushButton(t("gui.history_refresh"))
refresh_btn.clicked.connect(self.refresh)
clean_btn = QPushButton(t("gui.history_clean"))
clean_btn.clicked.connect(self._clean)
self.delete_selected_btn = QPushButton()
self.delete_selected_btn.setProperty("variant", "danger")
self.delete_selected_btn.clicked.connect(self._delete_selected)
self.delete_selected_btn.hide()

top = QHBoxLayout()
top.setSpacing(8)
top.addWidget(refresh_btn)
top.addWidget(clean_btn)
top.addWidget(self.delete_selected_btn)
top.addStretch(1)

self.list_box = QVBoxLayout()
Expand All @@ -64,6 +72,8 @@ def _build(self) -> None:
def refresh(self) -> None:
t = self.t
clear_layout(self.list_box, keep_tail=1)
self._selected.clear()
self._sync_delete_button()

groups = self.vm.history_groups()
if not groups:
Expand All @@ -77,6 +87,13 @@ def refresh(self) -> None:
row.setContentsMargins(12, 8, 12, 8)
row.setSpacing(8)

select = QCheckBox()
select.setToolTip(t("gui.history_delete_selected", n="…"))
select.toggled.connect(
lambda on, s=group.source: self._toggle_selected(s, on)
)
row.addWidget(select)

name = ElidedLabel(group.name)
name.setToolTip(group.source)
langs = " · ".join(
Expand Down Expand Up @@ -111,13 +128,47 @@ def refresh(self) -> None:
row.addWidget(view_btn)
row.addWidget(reveal_btn)

remove_btn = QPushButton("✕")
remove_btn.setProperty("variant", "quiet")
remove_btn.setFixedWidth(28)
remove_btn.setToolTip(t("gui.history_delete"))
remove_btn.clicked.connect(
lambda _=False, s=group.source: self._delete_sources({s})
)
row.addWidget(remove_btn)

self.list_box.insertWidget(self.list_box.count() - 1, frame)

def _clean(self) -> None:
removed = self.vm.history_clean_missing()
self.window_ref.toast(self.t("gui.history_cleaned", n=removed))
self.refresh()

# Deleting removes history records only; files on disk stay untouched.

def _toggle_selected(self, source: str, on: bool) -> None:
if on:
self._selected.add(source)
else:
self._selected.discard(source)
self._sync_delete_button()

def _sync_delete_button(self) -> None:
count = len(self._selected)
self.delete_selected_btn.setVisible(count > 0)
if count:
self.delete_selected_btn.setText(
self.t("gui.history_delete_selected", n=count)
)

def _delete_selected(self) -> None:
self._delete_sources(set(self._selected))

def _delete_sources(self, sources: set[str]) -> None:
removed = self.vm.history_delete_sources(sources)
self.window_ref.toast(self.t("gui.history_deleted_n", n=removed))
self.refresh()

# ------------------------------------------------------------------ #
# Viewer dialog
# ------------------------------------------------------------------ #
Expand Down Expand Up @@ -151,18 +202,37 @@ def __init__(self, page: HistoryPage, group):
self.body.setOpenExternalLinks(False)
self.body.setProperty("role", "preview")
self.body.setFrameShape(QTextBrowser.Shape.NoFrame)

self.editor = QPlainTextEdit()
self.editor.setProperty("role", "preview")
self.editor.hide()

self.play_btn = QPushButton(self.t("gui.play"))
self.play_btn.clicked.connect(self._play)
self.edit_btn = QPushButton(self.t("gui.edit"))
self.edit_btn.clicked.connect(self._start_edit)
self.save_btn = QPushButton(self.t("gui.save"))
self.save_btn.setProperty("variant", "primary")
self.save_btn.clicked.connect(self._save_edit)
self.save_btn.hide()
self.cancel_btn = QPushButton(self.t("gui.cancel"))
self.cancel_btn.clicked.connect(self._cancel_edit)
self.cancel_btn.hide()
close_btn = QPushButton(self.t("gui.close"))
close_btn.clicked.connect(self.accept)
bottom = QHBoxLayout()
bottom.addWidget(self.play_btn)
bottom.addWidget(self.edit_btn)
bottom.addStretch(1)
bottom.addWidget(self.cancel_btn)
bottom.addWidget(self.save_btn)
bottom.addWidget(close_btn)

root = QVBoxLayout(self)
root.setSpacing(8)
root.addLayout(self.lang_row)
root.addWidget(self.status_label)
root.addWidget(self.body, 1)
root.addWidget(self.editor, 1)
root.addLayout(bottom)

self._rebuild_buttons()
Expand Down Expand Up @@ -195,6 +265,8 @@ def _rebuild_buttons(self) -> None:
insert_at += 1

def _load(self, lang: str) -> None:
if self.editor.isVisible():
self._cancel_edit()
self.lang = lang
path = self.group.existing[lang]
try:
Expand All @@ -203,6 +275,74 @@ def _load(self, lang: str) -> None:
self.body.setPlainText(str(exc))
self._rebuild_buttons()

# ------------------------------------------------------------------ #
# Play (source video + this language's subtitles)
# ------------------------------------------------------------------ #

def _play(self) -> None:
from .player import PlayerDialog

if not Path(self.group.source).exists():
self.page.window_ref.toast(
self.t("gui.player_missing_video"), ok=False
)
return
srt = self.group.existing.get(self.lang or "", "")
PlayerDialog(
self, self.page.window_ref, self.group.source,
srt if srt.endswith(".srt") else None,
).exec()

# ------------------------------------------------------------------ #
# Edit the underlying file in place
# ------------------------------------------------------------------ #

def _current_path(self) -> str | None:
return self.group.existing.get(self.lang or "")

def _start_edit(self) -> None:
path = self._current_path()
if path is None:
return
try:
# Full file, not read_preview: previews truncate long files and
# saving a truncated buffer would destroy the tail.
content = Path(path).read_text(encoding="utf-8", errors="replace")
except Exception as exc:
self.status_label.setText(str(exc))
return
self.editor.setPlainText(content)
self.busy = True # freezes language switching + translate buttons
self._rebuild_buttons()
self._set_editing(True)

def _save_edit(self) -> None:
path = self._current_path()
if path is None:
return
try:
Path(path).write_text(self.editor.toPlainText(), encoding="utf-8")
except Exception as exc:
self.status_label.setText(str(exc))
return
self.busy = False
self._set_editing(False)
self._load(self.lang)
self.page.window_ref.toast(self.t("gui.settings_saved"))

def _cancel_edit(self) -> None:
self.busy = False
self._set_editing(False)
self._rebuild_buttons()

def _set_editing(self, editing: bool) -> None:
self.editor.setVisible(editing)
self.body.setVisible(not editing)
self.save_btn.setVisible(editing)
self.cancel_btn.setVisible(editing)
self.edit_btn.setVisible(not editing)
self.play_btn.setVisible(not editing)

def _render(self, content: str, path: str) -> None:
"""Subtitles read like a transcript, not like the raw file format:
one block per cue, dim second-precision time range, normal text."""
Expand Down
Loading
Loading