feat(ui): Steam-style conflict dialog
Replaces the radio+continue layout with a Steam Cloud Conflict
lookalike: uppercase title with circled-! glyph, three-line body
copy, two large card-buttons (Cloud Save / Local Save) each showing
icon + label + 'Modified <date>', Cancel bottom-right. Card click
commits the choice and dismisses — no separate Continue button.
Background switched to Steam-blue (#1b2838) instead of Prism dark
so the dialog reads as a 'pay attention' moment rather than another
status pane.
Card glyphs are unicode placeholders (☁ / ▢) — easy SVG swap later
if we want crisp HDD/cloud icons.
API change:
prompt_conflict_qt(local_summary, remote_summary)
→ prompt_conflict_qt(local_modified, remote_modified, save_label)
Caller now passes pre-formatted timestamp strings + an optional
noun phrase ('Minecraft save').
This commit is contained in:
+165
-48
@@ -278,77 +278,194 @@ def prompt_login_qt() -> str | None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
_CONFLICT_QSS = """
|
||||||
|
QDialog { background: #1b2838; }
|
||||||
|
QLabel#title {
|
||||||
|
color: white;
|
||||||
|
font-size: 20pt;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
QLabel#warning {
|
||||||
|
color: #1b2838;
|
||||||
|
background: #d6dde6;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: bold;
|
||||||
|
qproperty-alignment: AlignCenter;
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 32px;
|
||||||
|
}
|
||||||
|
QLabel#body {
|
||||||
|
color: #a9b3bf;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
QLabel#note {
|
||||||
|
color: #7d8a99;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
QFrame#card {
|
||||||
|
background: #2a3f5a;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QFrame#card:hover {
|
||||||
|
background: #34507a;
|
||||||
|
border: 1px solid #5b8fc7;
|
||||||
|
}
|
||||||
|
QLabel#cardTitle {
|
||||||
|
color: white;
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QLabel#cardSubtitle {
|
||||||
|
color: #a9b3bf;
|
||||||
|
font-size: 9pt;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QLabel#cardIcon {
|
||||||
|
color: white;
|
||||||
|
font-size: 24pt;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QPushButton#cancel {
|
||||||
|
background: #2a475e;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #4a6580;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
QPushButton#cancel:hover { background: #355d7d; }
|
||||||
|
QPushButton#cancel:pressed { background: #1f3548; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def prompt_conflict_qt(
|
def prompt_conflict_qt(
|
||||||
local_summary: str,
|
local_modified: str,
|
||||||
remote_summary: str,
|
remote_modified: str,
|
||||||
|
save_label: str = "Minecraft save",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Ask how to resolve a divergence.
|
"""Steam-Cloud-Conflict-styled dialog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
local_modified: human-readable "Saturday, February 12 at 12:28 AM"
|
||||||
|
remote_modified: same, but for the cloud snapshot
|
||||||
|
save_label: noun phrase for body copy (e.g. "Minecraft save").
|
||||||
|
|
||||||
Returns one of: ``"keep_local"``, ``"use_remote"``, ``"cancel"``.
|
Returns one of: ``"keep_local"``, ``"use_remote"``, ``"cancel"``.
|
||||||
|
Visually mirrors Steam's Cloud Conflict modal: header with warning
|
||||||
|
glyph + uppercase title, two large card buttons (cloud / local),
|
||||||
|
Cancel bottom-right. Card click commits and dismisses.
|
||||||
"""
|
"""
|
||||||
QtWidgets, QtCore, _ = import_qt()
|
QtWidgets, QtCore, Signal = import_qt()
|
||||||
app_existed = QtWidgets.QApplication.instance() is not None
|
app_existed = QtWidgets.QApplication.instance() is not None
|
||||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||||
if not app_existed:
|
if not app_existed:
|
||||||
_apply_prism_dark(app)
|
_apply_prism_dark(app)
|
||||||
|
|
||||||
|
class _Card(QtWidgets.QFrame):
|
||||||
|
clicked = Signal()
|
||||||
|
|
||||||
|
def __init__(self, glyph: str, title: str, subtitle: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.setObjectName("card")
|
||||||
|
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Fixed,
|
||||||
|
)
|
||||||
|
row = QtWidgets.QHBoxLayout(self)
|
||||||
|
row.setContentsMargins(18, 14, 18, 14)
|
||||||
|
row.setSpacing(16)
|
||||||
|
icon = QtWidgets.QLabel(glyph)
|
||||||
|
icon.setObjectName("cardIcon")
|
||||||
|
icon.setFixedWidth(40)
|
||||||
|
icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||||
|
row.addWidget(icon)
|
||||||
|
text_col = QtWidgets.QVBoxLayout()
|
||||||
|
text_col.setSpacing(2)
|
||||||
|
t = QtWidgets.QLabel(title)
|
||||||
|
t.setObjectName("cardTitle")
|
||||||
|
s = QtWidgets.QLabel(subtitle)
|
||||||
|
s.setObjectName("cardSubtitle")
|
||||||
|
text_col.addWidget(t)
|
||||||
|
text_col.addWidget(s)
|
||||||
|
row.addLayout(text_col, 1)
|
||||||
|
|
||||||
|
def mousePressEvent(self, _event: Any) -> None: # noqa: N802
|
||||||
|
self.clicked.emit()
|
||||||
|
|
||||||
dialog = QtWidgets.QDialog()
|
dialog = QtWidgets.QDialog()
|
||||||
dialog.setWindowTitle("Cloud sync — conflict")
|
dialog.setWindowTitle("Cloud sync — conflict")
|
||||||
dialog.setFixedSize(520, 320)
|
dialog.setFixedSize(640, 460)
|
||||||
|
dialog.setStyleSheet(_CONFLICT_QSS)
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout(dialog)
|
outer = QtWidgets.QVBoxLayout(dialog)
|
||||||
layout.setContentsMargins(20, 20, 20, 20)
|
outer.setContentsMargins(28, 24, 28, 20)
|
||||||
layout.setSpacing(10)
|
outer.setSpacing(14)
|
||||||
|
|
||||||
title = QtWidgets.QLabel("Local and cloud diverged")
|
header = QtWidgets.QHBoxLayout()
|
||||||
font = title.font()
|
header.setSpacing(12)
|
||||||
font.setBold(True)
|
warning = QtWidgets.QLabel("!")
|
||||||
font.setPointSize(font.pointSize() + 2)
|
warning.setObjectName("warning")
|
||||||
title.setFont(font)
|
header.addWidget(warning)
|
||||||
layout.addWidget(title)
|
title = QtWidgets.QLabel("CLOUD CONFLICT")
|
||||||
|
title.setObjectName("title")
|
||||||
|
header.addWidget(title)
|
||||||
|
header.addStretch(1)
|
||||||
|
outer.addLayout(header)
|
||||||
|
|
||||||
summary = QtWidgets.QLabel(
|
body = QtWidgets.QLabel(
|
||||||
f"Local changes since last sync:\n {local_summary}\n\n"
|
f"Your local {save_label} conflicts with what is stored in the cloud. "
|
||||||
f"Cloud has newer changes from another machine:\n {remote_summary}\n\n"
|
f"Whichever save data you choose to keep will be synced to this device "
|
||||||
f"What should we do?"
|
f"and the cloud. The option you choose not to keep will be overwritten."
|
||||||
)
|
)
|
||||||
summary.setWordWrap(True)
|
body.setObjectName("body")
|
||||||
layout.addWidget(summary)
|
body.setWordWrap(True)
|
||||||
|
outer.addWidget(body)
|
||||||
|
|
||||||
keep_local = QtWidgets.QRadioButton(
|
outer.addSpacing(6)
|
||||||
"Keep local — skip pull. Next exit will overwrite cloud."
|
|
||||||
)
|
|
||||||
use_remote = QtWidgets.QRadioButton(
|
|
||||||
"Use cloud — overwrite local files with the cloud version."
|
|
||||||
)
|
|
||||||
cancel_launch = QtWidgets.QRadioButton(
|
|
||||||
"Cancel launch — don't start Minecraft until I resolve manually."
|
|
||||||
)
|
|
||||||
keep_local.setChecked(True)
|
|
||||||
layout.addWidget(keep_local)
|
|
||||||
layout.addWidget(use_remote)
|
|
||||||
layout.addWidget(cancel_launch)
|
|
||||||
|
|
||||||
layout.addStretch(1)
|
cloud_card = _Card("☁", "Cloud Save", f"Modified {remote_modified}")
|
||||||
|
local_card = _Card("▢", "Local Save", f"Modified {local_modified}")
|
||||||
|
outer.addWidget(cloud_card)
|
||||||
|
outer.addWidget(local_card)
|
||||||
|
|
||||||
button_row = QtWidgets.QHBoxLayout()
|
outer.addStretch(1)
|
||||||
button_row.addStretch(1)
|
|
||||||
confirm = QtWidgets.QPushButton("Continue")
|
foot = QtWidgets.QHBoxLayout()
|
||||||
confirm.setDefault(True)
|
note = QtWidgets.QLabel(
|
||||||
button_row.addWidget(confirm)
|
"Note: You will need to choose a version to keep before launching the game."
|
||||||
layout.addLayout(button_row)
|
)
|
||||||
|
note.setObjectName("note")
|
||||||
|
note.setWordWrap(True)
|
||||||
|
foot.addWidget(note, 1)
|
||||||
|
cancel = QtWidgets.QPushButton("Cancel")
|
||||||
|
cancel.setObjectName("cancel")
|
||||||
|
foot.addWidget(cancel)
|
||||||
|
outer.addLayout(foot)
|
||||||
|
|
||||||
chosen: dict[str, str] = {"value": "cancel"}
|
chosen: dict[str, str] = {"value": "cancel"}
|
||||||
|
|
||||||
def on_confirm() -> None:
|
def pick_remote() -> None:
|
||||||
if keep_local.isChecked():
|
chosen["value"] = "use_remote"
|
||||||
chosen["value"] = "keep_local"
|
|
||||||
elif use_remote.isChecked():
|
|
||||||
chosen["value"] = "use_remote"
|
|
||||||
else:
|
|
||||||
chosen["value"] = "cancel"
|
|
||||||
dialog.accept()
|
dialog.accept()
|
||||||
|
|
||||||
confirm.clicked.connect(on_confirm)
|
def pick_local() -> None:
|
||||||
|
chosen["value"] = "keep_local"
|
||||||
|
dialog.accept()
|
||||||
|
|
||||||
|
def pick_cancel() -> None:
|
||||||
|
chosen["value"] = "cancel"
|
||||||
|
dialog.reject()
|
||||||
|
|
||||||
|
cloud_card.clicked.connect(pick_remote)
|
||||||
|
local_card.clicked.connect(pick_local)
|
||||||
|
cancel.clicked.connect(pick_cancel)
|
||||||
|
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
return chosen["value"]
|
return chosen["value"]
|
||||||
|
|||||||
Reference in New Issue
Block a user