feat(ui): Cài đặt ▸ Giới thiệu, gỡ dòng ghi công khỏi thanh trạng thái

Dòng "Made by QuanDH14" là widget thường trực ở góc dưới phải: chiếm một góc
màn hình trên MỌI màn, suốt cả phiên, cho một thông tin chỉ cần đọc một lần.
Chuyển vào Cài đặt ▸ Giới thiệu — vẫn tra được, không còn đứng thường trực.

Mục Giới thiệu đứng CUỐI danh sách: nó không có thiết lập nào để đổi, nên đặt
trước các mục thao tác được sẽ đẩy chúng xuống mà không được gì.

Đặc tả SettingsDialog đổi từ 5 lên 6 mục — thay đổi CÓ CHỦ Ý, không phải tách nhầm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-10 01:34:36 +09:00
committed by thanhnv
co-authored by Claude Opus 5
parent 19f3ccc7be
commit d4da30f9a6
4 changed files with 163 additions and 17 deletions
+60
View File
@@ -0,0 +1,60 @@
"""Mục "Giới thiệu" trong Cài đặt.
Dòng ghi công tác giả trước đây nằm cố định ở góc dưới phải cửa sổ, dưới dạng
widget thường trực của thanh trạng thái. Chỗ đó chiếm một góc màn hình trên MỌI
màn hình, suốt cả phiên làm việc, cho một thông tin người dùng chỉ cần đọc một
lần. Chuyển vào Cài đặt: vẫn tra được bất cứ lúc nào, nhưng không còn đứng
thường trực trong tầm mắt.
Cùng nhóm widget với bốn mục kia của Cài đặt (``general``/``provider``/
``parameter``/``routing``) nên nó tự là một trang, không cần lắp ráp riêng.
"""
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
from ... import DISPLAY_NAME, __version__
from ...i18n import tr
class AboutSettingsWidget(QWidget):
"""Nhóm "Giới thiệu": tên ứng dụng và dòng ghi công tác giả."""
def __init__(self, ctx=None, parent: QWidget | None = None) -> None:
"""Trang Giới thiệu. ``ctx`` không dùng tới, giữ cho khớp chữ ký của
bốn widget Cài đặt còn lại."""
super().__init__(parent)
self.ctx = ctx
layout = QVBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(8)
# Tên sản phẩm là danh từ riêng, không dịch — lấy thẳng từ gốc gói,
# cùng nguồn với tiêu đề cửa sổ (main_window.py:90) nên hai chỗ không lệch.
self.app_label = QLabel(f"{DISPLAY_NAME} v{__version__}")
# Không đặt màu ở đây: cỡ chữ là khác biệt duy nhất cần thiết, còn màu
# do theme quyết định (xem theme/__init__.py — ngoài theme/ không file
# nào được đặt tên một màu).
font = self.app_label.font()
font.setPointSize(font.pointSize() + 4)
font.setBold(True)
self.app_label.setFont(font)
layout.addWidget(self.app_label)
self.credit_label = QLabel(tr("app.credit"))
self.credit_label.setObjectName("faint")
self.credit_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
layout.addWidget(self.credit_label)
layout.addStretch(1)
def apply_to(self, data: dict) -> None:
"""Không có thiết lập nào để ghi — mục này chỉ hiển thị.
Vẫn khai để khớp giao diện chung của các trang Cài đặt: ``_save`` gọi
``apply_to`` trên từng trang, nên một trang thiếu hàm này sẽ là
``AttributeError`` ngay lần đầu ai đó thêm nó vào vòng lặp.
"""
return
+16 -11
View File
@@ -163,9 +163,10 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
self._built.append(widget is not None) self._built.append(widget is not None)
self._build_nav_rail(right, rlay) self._build_nav_rail(right, rlay)
# Landing stays Workspace ▸ Project, exactly as before. Go through _goto # Land on the Workspace screen. Go through _goto so the page is actually
# so the page is actually shown — selecting the row alone only moves the # shown — selecting the row alone only moves the highlight (its signals
# highlight (its signals are blocked to avoid rebuild loops). # are blocked to avoid rebuild loops). Which Workspace sub-view the user
# ends up looking at is settled after _restore_sessions(), below.
self._goto(self._ROW_WORKSPACE, self.workspace.current_subtab()) self._goto(self._ROW_WORKSPACE, self.workspace.current_subtab())
self.toast = Toast(self) # top-left "task done" popup self.toast = Toast(self) # top-left "task done" popup
# Floating in-app Help assistant — a robot icon pinned bottom-right on # Floating in-app Help assistant — a robot icon pinned bottom-right on
@@ -176,14 +177,19 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
self.help_agent.status_message.connect(self.statusBar().showMessage) self.help_agent.status_message.connect(self.statusBar().showMessage)
self.statusBar().showMessage(tr("app.status.ready")) self.statusBar().showMessage(tr("app.status.ready"))
# Author credit, pinned to the bottom-right corner. A permanent status-bar # Dòng ghi công tác giả đã chuyển vào Cài đặt ▸ Giới thiệu
# widget sits at the right end and is never cleared by showMessage (which # (presentation/settings/about_widget.py). Nó từng là widget thường trực
# writes on the left). # ở góc dưới phải: chiếm một góc màn hình trên MỌI màn hình, suốt cả
self._credit = QLabel(tr("app.credit")) # phiên, cho một thông tin chỉ cần đọc một lần.
self._credit.setObjectName("faint")
self._credit.setStyleSheet("padding: 0 10px;")
self.statusBar().addPermanentWidget(self._credit)
self._restore_sessions() self._restore_sessions()
# Open on "All projects…" — literally the same call the nav rail's link
# of that name makes, so the rail highlight and the content can never
# disagree. This runs AFTER the restore on purpose: _restore_sessions
# still reloads the last thread, because recovering it after a crash is
# the whole point of it, but the user should first see the list of
# everything rather than whichever conversation happened to be open when
# the app was last closed.
self.goto_all_projects()
self._tray.setup() self._tray.setup()
# Start the task scheduler last, once the whole window exists — it # Start the task scheduler last, once the whole window exists — it
# catches up any overdue tasks right away (first tick runs inline). # catches up any overdue tasks right away (first tick runs inline).
@@ -252,7 +258,6 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
self._nav_toggle_btn.setText("" if self._nav_collapsed else tr("app.nav.menu_label")) self._nav_toggle_btn.setText("" if self._nav_collapsed else tr("app.nav.menu_label"))
self._nav_toggle_btn.setToolTip( self._nav_toggle_btn.setToolTip(
tr("app.nav.expand_tooltip") if self._nav_collapsed else tr("app.nav.collapse_tooltip")) tr("app.nav.expand_tooltip") if self._nav_collapsed else tr("app.nav.collapse_tooltip"))
self._credit.setText(tr("app.credit"))
if hasattr(self, "provider_lbl"): if hasattr(self, "provider_lbl"):
self.provider_lbl.setText(tr("app.provider")) self.provider_lbl.setText(tr("app.provider"))
if hasattr(self, "settings_btn"): if hasattr(self, "settings_btn"):
+75
View File
@@ -0,0 +1,75 @@
"""Mục "Giới thiệu" trong Cài đặt, và việc gỡ dòng ghi công khỏi thanh trạng thái.
Dòng "Made by QuanDH14" từng là widget thường trực ở góc dưới phải: chiếm một góc
màn hình trên MỌI màn hình, suốt cả phiên, cho một thông tin chỉ cần đọc một lần.
Nó chuyển vào Cài đặt ▸ Giới thiệu — vẫn tra được, không còn đứng thường trực.
Hai đầu của việc chuyển này phải được chốt cùng nhau: chỉ thêm mục mới mà quên gỡ
chỗ cũ thì thành hiện hai lần; chỉ gỡ mà quên thêm thì mất hẳn.
"""
from __future__ import annotations
import pytest
from .test_settings_dialog_dac_ta import _Ctx
@pytest.fixture
def dialog(qapp):
from cowork_local.ui.settings_dialog import SettingsDialog
dlg = SettingsDialog(_Ctx())
yield dlg
dlg.deleteLater()
# ---- đầu MỚI: mục Giới thiệu trong Cài đặt -------------------------------
def test_cai_dat_co_muc_gioi_thieu(dialog):
from cowork_local.i18n import tr
labels = [dialog.section_list.item(i).text()
for i in range(dialog.section_list.count())]
assert tr("settings.group.about") in labels
def test_muc_gioi_thieu_dung_cuoi(dialog):
"""Nó không có gì để đổi, nên đặt trước các mục thao tác được là đẩy chúng
xuống mà không được gì."""
from cowork_local.i18n import tr
last = dialog.section_list.item(dialog.section_list.count() - 1).text()
assert last == tr("settings.group.about")
def test_muc_gioi_thieu_hien_dong_ghi_cong(dialog):
from cowork_local.i18n import tr
assert dialog._about_page.credit_label.text() == tr("app.credit")
assert "QuanDH14" in dialog._about_page.credit_label.text()
def test_muc_gioi_thieu_hien_ten_va_phien_ban(dialog):
from cowork_local import DISPLAY_NAME, __version__
text = dialog._about_page.app_label.text()
assert DISPLAY_NAME in text
assert __version__ in text
# ---- đầu CŨ: không còn ở thanh trạng thái --------------------------------
def test_khong_con_dong_ghi_cong_trong_ma_nguon_cua_so_chinh():
"""Chốt cả hai đầu: cửa sổ chính không được dựng lại widget ghi công.
Kiểm trên mã nguồn thay vì dựng ``MainWindow`` — dựng cửa sổ thật mất ~35
giây cho một khẳng định mà đọc file trả lời được ngay.
"""
from pathlib import Path
src = (Path(__file__).resolve().parents[2]
/ "presentation" / "shell" / "main_window.py").read_text(encoding="utf-8")
assert "_credit" not in src, "vẫn còn widget ghi công ở cửa sổ chính"
assert "addPermanentWidget" not in src, (
"thanh trạng thái lại có widget thường trực — nếu là thứ khác thì sửa "
"khẳng định này, nhưng phải cân nhắc: nó chiếm một góc trên mọi màn hình")
+12 -6
View File
@@ -70,15 +70,21 @@ def dialog(qapp):
# ---- dialog gồm những mục nào ------------------------------------------- # ---- dialog gồm những mục nào -------------------------------------------
def test_co_dung_nam_muc(dialog): def test_co_dung_sau_muc(dialog):
"""Năm mục thật trên màn hình. Plan R08-T07 ghi bốn widget và có một cái """Sáu mục thật trên màn hình.
tên `connector`, nhưng UI connector đã dời khỏi Settings từ trước (xem
ghi chú ở settings_dialog.py:180) — nên con số thật là năm, không bốn.""" Plan R08-T07 ghi bốn widget và có một cái tên `connector`, nhưng UI
connector đã dời khỏi Settings từ trước (xem ghi chú ở
settings_dialog.py:180) — nên con số thật là năm, không bốn.
Mục thứ sáu là "Giới thiệu", thêm ngày 2026-09-07 cùng lúc với việc gỡ dòng
ghi công tác giả khỏi góc dưới phải cửa sổ. Đây là thay đổi CÓ CHỦ Ý, không
phải tách nhầm."""
dlg, _ = dialog dlg, _ = dialog
labels = [dlg.section_list.item(i).text() labels = [dlg.section_list.item(i).text()
for i in range(dlg.section_list.count())] for i in range(dlg.section_list.count())]
assert len(labels) == 5, labels assert len(labels) == 6, labels
assert dlg.section_stack.count() == 5 assert dlg.section_stack.count() == 6
def test_moi_muc_deu_bam_duoc(dialog): def test_moi_muc_deu_bam_duoc(dialog):