feat: improve work cards and browser startup

This commit is contained in:
2026-09-07 19:32:16 +08:00
parent b42b2d33da
commit 9145508665
9 changed files with 427 additions and 24 deletions
+1
View File
@@ -256,6 +256,7 @@ class BrowserManager:
"--remote-debugging-address=127.0.0.1",
"--no-first-run",
"--no-default-browser-check",
"--disable-session-crashed-bubble",
f"--fingerprint={expected['fingerprint_seed']}",
*expected["args"],
"https://www.douyin.com/",
+265 -24
View File
@@ -2,6 +2,7 @@
import argparse
import asyncio
import hashlib
import json
import queue
import sys
@@ -21,6 +22,8 @@ from PySide6.QtWidgets import (
QDialogButtonBox,
QFileDialog,
QFormLayout,
QFrame,
QGridLayout,
QHBoxLayout,
QHeaderView,
QLabel,
@@ -29,10 +32,13 @@ from PySide6.QtWidgets import (
QMessageBox,
QPlainTextEdit,
QPushButton,
QScrollArea,
QSizePolicy,
QSpinBox,
QTableWidget,
QTableWidgetItem,
QTabWidget,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
@@ -45,6 +51,154 @@ from account_log import UI_LIMIT, DailyLog
from account_store import decode, validate_rule
IMAGE_HOST_SUFFIXES = (
"douyinpic.com",
"byteimg.com",
"douyincdn.com",
"ibytedtos.com",
)
def allowed_image_url(value):
url = QUrl(value or "")
host = url.host().lower()
return bool(value) and url.scheme() == "https" and any(
host.endswith("." + suffix) for suffix in IMAGE_HOST_SUFFIXES
)
class WorkCard(QFrame):
def __init__(self, item, checked=False, parent=None):
super().__init__(parent)
self.setObjectName("workCard")
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setMinimumHeight(158)
self.check_box = QCheckBox("监控")
self.check_box.setChecked(checked)
self.cover_label = QLabel("暂无封面")
self.cover_label.setObjectName("workCover")
self.cover_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.cover_label.setFixedWidth(132)
self.cover_label.setMinimumHeight(158)
self.cover_label.setSizePolicy(
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding
)
self.cover_label.setStyleSheet(
"background: #f3f4f6; color: #8a8f98; border-radius: 4px;"
)
self._cover_pixmap = QPixmap()
description = item.get("desc") or "未填写作品描述"
self.description_label = QLabel(description)
self.description_label.setWordWrap(True)
self.description_label.setToolTip(description)
self._description = description
self._description_collapsed_height = (
self.description_label.fontMetrics().lineSpacing() * 2 + 4
)
self._description_expanded = False
self.description_toggle = QToolButton()
self.description_toggle.setText("展开")
self.description_toggle.setAutoRaise(True)
self.description_toggle.setStyleSheet("color: #1677ff; border: 0; padding: 0;")
self.description_toggle.clicked.connect(self.toggle_description)
statistics = item.get("statistics") or {}
stats = QLabel(
"\n".join(
[
"点赞:" + str(statistics.get("digg_count", 0)),
"评论:" + str(statistics.get("comment_count", 0)),
"收藏:" + str(statistics.get("collect_count", 0)),
"分享:" + str(statistics.get("share_count", 0)),
]
)
)
times = QLabel(
"发布时间:{}\n更新时间:{}".format(
date_text(item.get("create_time")),
date_text(item.get("updated")),
)
)
id_label = QLabel("ID:" + str(item["aweme_id"]))
for label in (id_label, stats, times):
label.setStyleSheet("color: #68707d;")
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
info = QVBoxLayout()
info.setContentsMargins(10, 8, 10, 8)
header = QHBoxLayout()
header.addWidget(id_label)
header.addStretch()
header.addWidget(self.check_box)
info.addLayout(header)
info.addWidget(self.description_label)
description_actions = QHBoxLayout()
description_actions.addStretch()
description_actions.addWidget(self.description_toggle)
info.addLayout(description_actions)
info.addWidget(stats)
info.addWidget(times)
info.addStretch()
self._refresh_description()
card_layout = QHBoxLayout(self)
card_layout.setContentsMargins(0, 0, 0, 0)
card_layout.setSpacing(0)
card_layout.addWidget(self.cover_label)
card_layout.addLayout(info, 1)
self.setStyleSheet(
"#workCard { border: 1px solid #d9dde5; border-radius: 6px; }"
)
def toggle_description(self):
self._description_expanded = not self._description_expanded
self.description_toggle.setText(
"收起" if self._description_expanded else "展开"
)
self._refresh_description()
def _refresh_description(self):
width = max(1, self.description_label.width())
full_height = self.description_label.fontMetrics().boundingRect(
0,
0,
width,
10000,
Qt.TextFlag.TextWordWrap,
self._description,
).height()
expandable = full_height > self._description_collapsed_height
self.description_toggle.setVisible(expandable)
if not expandable:
self._description_expanded = False
self.description_toggle.setText("展开")
self.description_label.setMaximumHeight(
16777215
if self._description_expanded
else self._description_collapsed_height
)
def resizeEvent(self, event):
super().resizeEvent(event)
self._refresh_description()
self._render_cover()
def set_cover(self, pixmap):
if pixmap.isNull():
return
self._cover_pixmap = pixmap
self.cover_label.setText("")
self._render_cover()
def _render_cover(self):
if self._cover_pixmap.isNull() or self.cover_label.size().isEmpty():
return
self.cover_label.setPixmap(
self._cover_pixmap.scaled(
self.cover_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
def date_text(value):
try:
stamp = float(value)
@@ -198,6 +352,7 @@ class Window(QMainWindow):
self.signature = None
self.icons = {}
self.avatar_pending = set()
self.work_cover_cache = Path(self.backend.root) / "work_covers"
self.network = QNetworkAccessManager(self)
self.setWindowTitle("抖音账号助手 · 自动操作默认关闭")
self.resize(1260, 780)
@@ -572,7 +727,7 @@ class Window(QMainWindow):
all_mode.setChecked(payload["mode"] == "all")
layout.addWidget(all_mode)
note = QLabel(
"取消上方选项后,只监控表格中勾选作品的点赞、评论、收藏等互动;关注通知不受作品筛选。完整平台数据只保存在本地数据库中。"
"取消上方选项后,只监控下方勾选作品的点赞、评论、收藏等互动;关注通知不受作品筛选。封面首次加载后保存在本机,后续打开优先读取缓存。"
)
note.setWordWrap(True)
layout.addWidget(note)
@@ -591,7 +746,10 @@ class Window(QMainWindow):
)
schedule.addStretch()
layout.addLayout(schedule)
table = QTableWidget(len(items), 10)
# Keep the existing selection model so saving behavior stays unchanged;
# only the visible presentation is now cards.
table = QTableWidget(len(items), 10, win)
table.setHorizontalHeaderLabels(
[
"选择",
@@ -606,6 +764,22 @@ class Window(QMainWindow):
"本地更新时间",
]
)
table.hide()
cards = []
card_area = QScrollArea()
card_area.setWidgetResizable(True)
card_area.setFrameShape(QFrame.Shape.NoFrame)
card_host = QWidget()
card_grid = QGridLayout(card_host)
card_grid.setContentsMargins(4, 4, 4, 4)
card_grid.setHorizontalSpacing(12)
card_grid.setVerticalSpacing(12)
for column in range(3):
card_grid.setColumnStretch(column, 1)
card_area.setWidget(card_host)
layout.addWidget(card_area, 1)
cover_cards = {}
for row, item in enumerate(items):
check = QTableWidgetItem()
check.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
@@ -616,29 +790,107 @@ class Window(QMainWindow):
)
check.setData(Qt.ItemDataRole.UserRole, item["aweme_id"])
table.setItem(row, 0, check)
statistics = item["statistics"]
statistics = item.get("statistics") or {}
values = [
date_text(item["create_time"]),
item["desc"] or "未填写作品描述",
date_text(item.get("create_time")),
item.get("desc") or "未填写作品描述",
item["aweme_id"],
statistics.get("digg_count", 0),
statistics.get("comment_count", 0),
statistics.get("collect_count", 0),
statistics.get("share_count", 0),
item["cover"] or "无封面地址",
item.get("cover") or "无封面地址",
date_text(item.get("updated")),
]
for column, value in enumerate(values, 1):
cell = QTableWidgetItem(str(value))
cell.setToolTip(str(value))
table.setItem(row, column, cell)
table.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.ResizeToContents
)
table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
card = WorkCard(item, check.checkState() == Qt.CheckState.Checked)
cards.append(card)
card_grid.addWidget(card, row // 3, row % 3)
cover_cards.setdefault(item.get("aweme_id", ""), []).append(card)
def sync_card(item):
if item.column() != 0 or not 0 <= item.row() < len(cards):
return
checked = item.checkState() == Qt.CheckState.Checked
if cards[item.row()].check_box.isChecked() != checked:
cards[item.row()].check_box.setChecked(checked)
def sync_table(checked, row):
item = table.item(row, 0)
if item is not None:
state = Qt.CheckState.Checked if checked else Qt.CheckState.Unchecked
if item.checkState() != state:
item.setCheckState(state)
table.itemChanged.connect(sync_card)
for row, card in enumerate(cards):
card.check_box.toggled.connect(lambda checked, row=row: sync_table(checked, row))
def load_cover(item):
ident = item["aweme_id"]
cache_path = self.work_cover_cache / (
hashlib.sha256(
f"{payload['account']}:{ident}".encode()
).hexdigest()
+ ".img"
)
pixmap = QPixmap()
try:
if cache_path.is_file():
pixmap.loadFromData(cache_path.read_bytes())
except OSError:
pass
if not pixmap.isNull():
for card in cover_cards.get(ident, []):
card.set_cover(pixmap)
return
url = item.get("cover") or ""
if not allowed_image_url(url):
return
request = QNetworkRequest(QUrl(url))
request.setTransferTimeout(10000)
request.setAttribute(
QNetworkRequest.Attribute.RedirectPolicyAttribute,
QNetworkRequest.RedirectPolicy.ManualRedirectPolicy,
)
reply = self.network.get(request)
reply.downloadProgress.connect(
lambda received, total: (
reply.abort()
if received > 2_000_000 or total > 2_000_000
else None
)
)
def loaded():
raw = bytes(reply.readAll())
image = QPixmap()
if reply.error() == reply.NetworkError.NoError:
image.loadFromData(raw)
if not image.isNull():
try:
self.work_cover_cache.mkdir(parents=True, exist_ok=True)
temp = cache_path.with_name(cache_path.name + ".tmp")
temp.write_bytes(raw)
temp.replace(cache_path)
except OSError:
pass
for card in cover_cards.get(ident, []):
card.set_cover(image)
reply.deleteLater()
reply.finished.connect(loaded)
for item in items:
load_cover(item)
table.setDisabled(all_mode.isChecked())
card_area.setDisabled(all_mode.isChecked())
all_mode.toggled.connect(table.setDisabled)
layout.addWidget(table)
all_mode.toggled.connect(card_area.setDisabled)
controls = QHBoxLayout()
select_all = QPushButton("全选")
clear = QPushButton("清空选择")
@@ -955,25 +1207,14 @@ class Window(QMainWindow):
self.send("resolve", {"task": task["id"], "succeeded": succeeded})
def avatar(self, account):
url = QUrl(account.get("avatar", ""))
host = url.host().lower()
allowed = any(
host.endswith("." + suffix)
for suffix in (
"douyinpic.com",
"byteimg.com",
"douyincdn.com",
"ibytedtos.com",
)
)
key = account["avatar"]
if (
key
and key not in self.icons
and key not in self.avatar_pending
and allowed
and url.scheme() == "https"
and allowed_image_url(key)
):
url = QUrl(key)
self.avatar_pending.add(key)
request = QNetworkRequest(url)
request.setTransferTimeout(10000)
+1
View File
@@ -164,6 +164,7 @@ async def matrix(chrome):
real_popen = subprocess.Popen
def offline_launch(args, **kwargs):
assert "--disable-session-crashed-bubble" in args
# Only this test replaces the home URL and disables background network traffic.
return real_popen(
[