One merge window, not fifteen
show_conflict_summary() built a fresh ConflictSummaryDialog every time and only reassigned self._conflict_dialog — the old dialog was still a child of the window, so it stayed on screen. check_for_external_changes() runs resolve_conflicts() on every Syncthing watch tick, so that was one window per merge, forever. Fifteen had piled up on trav's desktop. The dialog is now a session-long log: add_event() folds each merge in as its own timestamped entry, newest first, and the restore button names the merge it would undo. MainWindow reuses the open dialog and clears its reference on finished, so closing it lets the next merge open a fresh one instead of touching a deleted C++ object. Claude-Session: https://claude.ai/code/session_01M8Mzze7shr5pZoEgKQU5NW
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
"""A read-only summary of Syncthing conflicts LinTunes auto-merged, with the
|
||||
option to open or restore the pre-merge backup."""
|
||||
option to open or restore the pre-merge backup.
|
||||
|
||||
One dialog per session, not one per merge: Syncthing can deliver conflicts every
|
||||
few minutes, so ``add_event`` folds each new merge into the open window as a
|
||||
timestamped entry instead of stacking another window on the desktop.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
@@ -9,62 +15,97 @@ from PyQt6.QtWidgets import (
|
||||
QMessageBox)
|
||||
|
||||
|
||||
INTRO = ("LinTunes found changes made on more than one machine and merged them "
|
||||
"(play counts kept highest, newest edits win, nothing removed). Both "
|
||||
"versions were backed up first — restore them if a merge isn't what "
|
||||
"you wanted.")
|
||||
|
||||
|
||||
class ConflictSummaryDialog(QDialog):
|
||||
def __init__(self, summaries, manager, parent=None):
|
||||
super().__init__(parent)
|
||||
self._summaries = summaries
|
||||
self._manager = manager
|
||||
self._backup_dir = summaries[0].backup_dir if summaries else ""
|
||||
self._events = [] # newest last: (datetime, list[ConflictSummary])
|
||||
|
||||
self.setWindowTitle("Synced changes merged")
|
||||
self.resize(560, 440)
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
intro = QLabel(
|
||||
"LinTunes found changes made on more than one machine and merged "
|
||||
"them (play counts kept highest, newest edits win, nothing removed). "
|
||||
"Both versions were backed up first — restore them if a merge isn't "
|
||||
"what you wanted.")
|
||||
intro.setWordWrap(True)
|
||||
layout.addWidget(intro)
|
||||
self._intro = QLabel(INTRO)
|
||||
self._intro.setWordWrap(True)
|
||||
layout.addWidget(self._intro)
|
||||
|
||||
body = QTextEdit()
|
||||
body.setReadOnly(True)
|
||||
body.setPlainText(self._format(summaries))
|
||||
layout.addWidget(body, 1)
|
||||
self._body = QTextEdit()
|
||||
self._body.setReadOnly(True)
|
||||
layout.addWidget(self._body, 1)
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
open_btn = QPushButton("Open backup folder")
|
||||
open_btn.clicked.connect(self._open_backup)
|
||||
restore_btn = QPushButton("Restore pre-merge backup")
|
||||
restore_btn.clicked.connect(self._restore_backup)
|
||||
self._open_btn = QPushButton("Open backup folder")
|
||||
self._open_btn.clicked.connect(self._open_backup)
|
||||
self._restore_btn = QPushButton("Restore pre-merge backup")
|
||||
self._restore_btn.clicked.connect(self._restore_backup)
|
||||
keep_btn = QPushButton("Keep merged")
|
||||
keep_btn.setDefault(True)
|
||||
keep_btn.clicked.connect(self.accept)
|
||||
buttons.addWidget(open_btn)
|
||||
buttons.addWidget(restore_btn)
|
||||
buttons.addWidget(self._open_btn)
|
||||
buttons.addWidget(self._restore_btn)
|
||||
buttons.addStretch(1)
|
||||
buttons.addWidget(keep_btn)
|
||||
layout.addLayout(buttons)
|
||||
|
||||
open_btn.setEnabled(bool(self._backup_dir))
|
||||
restore_btn.setEnabled(bool(self._backup_dir))
|
||||
self.add_event(summaries)
|
||||
|
||||
@staticmethod
|
||||
def _format(summaries) -> str:
|
||||
# ---- events ----
|
||||
|
||||
def add_event(self, summaries, when=None):
|
||||
"""Fold another merge into this window as its own timestamped entry."""
|
||||
self._events.append((when or datetime.now(), list(summaries or [])))
|
||||
self._refresh()
|
||||
|
||||
@property
|
||||
def _backup_dir(self) -> str:
|
||||
"""The newest event's backup snapshot. One resolve_conflicts() call makes
|
||||
one timestamped dir, so every summary in an event shares it."""
|
||||
for _when, summaries in reversed(self._events):
|
||||
if summaries and summaries[0].backup_dir:
|
||||
return summaries[0].backup_dir
|
||||
return ""
|
||||
|
||||
def _refresh(self):
|
||||
self._body.setPlainText(self._format())
|
||||
count = len(self._events)
|
||||
self._intro.setText(
|
||||
INTRO if count < 2 else f"{INTRO}\n\n{count} merges this session.")
|
||||
newest = self._events[-1][0] if self._events else None
|
||||
self._restore_btn.setText(
|
||||
f"Restore backup from {newest.strftime('%-I:%M %p')}"
|
||||
if newest and count > 1 else "Restore pre-merge backup")
|
||||
self._open_btn.setEnabled(bool(self._backup_dir))
|
||||
self._restore_btn.setEnabled(bool(self._backup_dir))
|
||||
|
||||
def _format(self) -> str:
|
||||
blocks = []
|
||||
for s in summaries:
|
||||
lines = [f"● {s.file}"]
|
||||
lines.extend(f" {line}" for line in s.lines)
|
||||
blocks.append("\n".join(lines))
|
||||
multi = len(self._events) > 1
|
||||
for when, summaries in reversed(self._events): # newest first
|
||||
lines = []
|
||||
if multi:
|
||||
lines.append(f"── {when.strftime('%-I:%M %p')} " + "─" * 30)
|
||||
for s in summaries:
|
||||
lines.append(f"● {s.file}")
|
||||
lines.extend(f" {line}" for line in s.lines)
|
||||
lines.append("")
|
||||
blocks.append("\n".join(lines).rstrip())
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
# ---- actions ----
|
||||
|
||||
def _open_backup(self):
|
||||
if self._backup_dir:
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(self._backup_dir))
|
||||
|
||||
def _restore_backup(self):
|
||||
if not self._backup_dir:
|
||||
backup_dir = self._backup_dir
|
||||
if not backup_dir:
|
||||
return
|
||||
if QMessageBox.question(
|
||||
self, "Restore backup",
|
||||
@@ -73,7 +114,7 @@ class ConflictSummaryDialog(QDialog):
|
||||
"conflict later.") != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
from lintunes.storage import conflict_resolver
|
||||
conflict_resolver.restore_backup(Path(self._backup_dir), self._manager.data_dir)
|
||||
conflict_resolver.restore_backup(Path(backup_dir), self._manager.data_dir)
|
||||
self._manager.reload_from_disk()
|
||||
QMessageBox.information(self, "Restored", "Restored the pre-merge backup.")
|
||||
self.accept()
|
||||
|
||||
@@ -745,12 +745,27 @@ class MainWindow(QMainWindow):
|
||||
self.show_conflict_summary(summaries)
|
||||
|
||||
def show_conflict_summary(self, summaries):
|
||||
"""Show (or extend) the one merge window. Syncthing can deliver conflicts
|
||||
every few minutes; each merge becomes another entry in the open window
|
||||
rather than another window."""
|
||||
from lintunes.gui.conflict_dialog import ConflictSummaryDialog
|
||||
# Modeless so a merge never blocks playback or what you're doing.
|
||||
self._conflict_dialog = ConflictSummaryDialog(summaries, self._manager, self)
|
||||
self._conflict_dialog.show()
|
||||
if self._conflict_dialog is not None:
|
||||
self._conflict_dialog.add_event(summaries)
|
||||
else:
|
||||
# Modeless so a merge never blocks playback or what you're doing.
|
||||
self._conflict_dialog = ConflictSummaryDialog(
|
||||
summaries, self._manager, self)
|
||||
self._conflict_dialog.finished.connect(self._on_conflict_dialog_closed)
|
||||
self._conflict_dialog.show()
|
||||
self._conflict_dialog.raise_()
|
||||
|
||||
def _on_conflict_dialog_closed(self, _result):
|
||||
"""Drop the reference (and the widget) so the next merge opens a fresh
|
||||
window instead of touching a deleted C++ object."""
|
||||
dialog, self._conflict_dialog = self._conflict_dialog, None
|
||||
if dialog is not None:
|
||||
dialog.deleteLater()
|
||||
|
||||
# ---- info dialog ----
|
||||
|
||||
def _show_info(self, track_ids: list[int]):
|
||||
|
||||
Reference in New Issue
Block a user