feat: improve work cards and browser startup
This commit is contained in:
@@ -10,3 +10,11 @@
|
||||
## 产出目标
|
||||
- 在src中输出自动化处理的python脚本,要求每个功能的脚本可独立运行
|
||||
- 能用的功能、函数可使用公共包来实现,不需要独立实现
|
||||
|
||||
## Windows 远程部署与连接
|
||||
- 优先使用项目级 `.pi/mcp.json` 中的 `windows-ui` MCP 操作 Windows 主机;该连接用于启动、停止、查看和人工验收 Windows 应用。
|
||||
- Windows MCP 不满足需求或批量命令明显不便时,改用 SSH 执行文件传输、构建、安装和日志检查;连接地址、账号和密钥只从本机 SSH 配置读取,不写入代码、安装包或文档。
|
||||
- Windows 原生构建必须在 Windows x64 主机执行 `build_windows.py`,不要把 Linux 构建产物伪装成 Windows 安装包。
|
||||
- 部署顺序:先在本地完成测试,再将源码或构建产物传到 Windows;执行构建/安装后通过 Windows MCP 做 GUI 冒烟和人工验收。
|
||||
- 远程操作前先确认目标程序当前状态;不要自动登录、恢复业务规则或执行真实关注/私信等写操作。登录状态丢失时停止并交给用户登录。
|
||||
- 远程部署记录写入 `docs/YYYY-MM-DD-HH-II-<topic>.md`,记录连接方式、执行命令、产物路径和验证结果,但不记录密码、Cookie、UID 或登录目录内容。
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 作品监控 CARD 列表与封面本地缓存
|
||||
|
||||
## 调研记录
|
||||
|
||||
- 原作品监控窗口位于 `src/accounts_app.py::Window.works_dialog`,原实现使用 `QTableWidget` 展示 10 列数据。
|
||||
- 保存逻辑依赖勾选作品 ID、监控模式和刷新间隔;这部分行为保持不变。
|
||||
- 作品数据中的 `cover` 是平台返回的 HTTPS 图片地址,作品缓存数据本身保存在 SQLite 中,但封面此前每次打开窗口都没有本地文件缓存。
|
||||
- 当前 UI 已有 `QNetworkAccessManager`,可复用其网络请求能力;不新增运行依赖。
|
||||
|
||||
## 最终方案
|
||||
|
||||
- 用 `QScrollArea + QGridLayout` 展示 CARD,固定 3 列,因此每行最多 3 个卡片。
|
||||
- 每张卡片采用左侧 132×132 封面、右侧作品描述、发布时间、作品 ID、点赞/评论/收藏/分享、本地更新时间和监控勾选框的布局。
|
||||
- 封面缓存写入数据根目录下的 `work_covers/`,文件名由“账号 ID + 作品 ID”哈希生成,避免直接使用外部字符串作为文件名。
|
||||
- 打开列表时优先读取本地图片;只有缓存不存在且 URL 为允许的抖音图片 HTTPS 域名时才发起请求。成功下载后先写临时文件,再原子替换缓存文件。
|
||||
- 保留不可见的原选择模型用于复用既有保存逻辑和兼容原有离线 UI 回归测试;用户可见区域不再显示表格。
|
||||
|
||||
## 验证
|
||||
|
||||
- `.venv/bin/python -m unittest discover -s src -v`:56 个测试通过。
|
||||
- `python -m py_compile src/accounts_app.py`:通过。
|
||||
- offscreen Qt 下完成作品卡片窗口构造 smoke check。
|
||||
|
||||
## 安装说明
|
||||
|
||||
无需新增依赖,继续按项目现有方式安装 `requirements-desktop.txt` 即可。
|
||||
@@ -0,0 +1,38 @@
|
||||
# Windows 远程部署连接方案
|
||||
|
||||
## 参考来源
|
||||
|
||||
参考 `/home/rogee/Workspace/windows/AGENTS.md` 与其 `.mcp.json`:Windows 操作优先使用 Windows MCP;MCP 不满足需求或批量命令不便时使用 SSH。
|
||||
|
||||
## 当前项目配置
|
||||
|
||||
项目级 `.pi/mcp.json` 增加 `windows-ui`:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://10.1.1.101:8765/mcp",
|
||||
"excludeTools": ["screenshot"]
|
||||
}
|
||||
```
|
||||
|
||||
该配置只用于当前 Pi 项目,不写入 Windows 安装包。MCP 服务不可用时,不修改 MCP 配置来绕过问题,按本机 SSH 配置选择目标主机。
|
||||
|
||||
## 部署流程
|
||||
|
||||
1. 本地运行测试并检查工作区差异。
|
||||
2. Windows MCP 用于启动、停止、查看程序和人工 GUI 验收。
|
||||
3. SSH 用于源码/构建产物传输、Windows 原生构建、安装和日志检查。
|
||||
4. Windows x64 主机上执行 `build_windows.py`,生成 `release/DouyinAccounts-0.1.8-Windows-x64-Setup.exe`。
|
||||
5. 安装后通过 Windows MCP 做离线启动冒烟和界面验收;不自动登录账号、不恢复业务、不执行关注或私信。
|
||||
6. 记录产物路径、SHA256 和验证结果,不记录密码、Cookie、UID 或登录目录内容。
|
||||
|
||||
## 空白 Windows 环境
|
||||
|
||||
```powershell
|
||||
py -3.12 -m venv .venv
|
||||
.\\.venv\\Scripts\\python.exe -m pip install -r requirements-build-windows.txt
|
||||
.\\.venv\\Scripts\\python.exe -m unittest discover -s src -v
|
||||
.\\.venv\\Scripts\\python.exe build_windows.py
|
||||
```
|
||||
|
||||
普通用户只运行生成的安装包,不需要 Python、Qt、Patchright 或额外下载浏览器。
|
||||
@@ -0,0 +1,44 @@
|
||||
# Windows 安装记录:作品 CARD 与封面缓存
|
||||
|
||||
## 目标
|
||||
|
||||
将当前 `src/accounts_app.py` 的作品 CARD 列表和封面本机缓存改动安装到 Windows x64 主机。远程 MCP 当前会话未加载,按项目规则使用 SSH fallback。
|
||||
|
||||
## 连接与冲突处理
|
||||
|
||||
- 目标:Windows 10 x64,SSH 目标 `10.1.1.101`,账号和密钥来自本机 SSH 配置/授权环境。
|
||||
- 安装前发现 `DouyinAccounts.exe` 正在运行,已关闭其进程树;没有删除数据目录,也没有主动关闭或修改账号登录数据。
|
||||
|
||||
## 构建
|
||||
|
||||
- Windows 构建目录:`C:\Users\rogee\douyin-pc-build-20260906`
|
||||
- 同步文件:`src/accounts_app.py`
|
||||
- Windows 单元测试:56 个通过。
|
||||
- 原生构建:`build_windows.py` 成功,Inno Setup 编译耗时约 312 秒。
|
||||
- 安装包:`release/DouyinAccounts-0.1.8-Windows-x64-Setup.exe`
|
||||
- 安装包 SHA256:`6CA36F4388AB6D5DF7551E467E94B6FB27C1CCF353D974DAA2058661EB3D1A69`
|
||||
|
||||
## 安装与验证
|
||||
|
||||
使用静默参数 `/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS` 安装完成,未自动启动业务应用。
|
||||
|
||||
安装路径:`C:\Users\Rogee\AppData\Local\Programs\DouyinAccounts\DouyinAccounts.exe`
|
||||
|
||||
安装后的可执行文件与 Windows 构建目录中的 dist 可执行文件 SHA256 一致:
|
||||
|
||||
`386AD6F7E6FA2A2C2811E4AD171E06698799D8584F5D6B3FFF01EFE9576B1790`
|
||||
|
||||
已执行安装包自带离线冒烟测试,结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"qt": true,
|
||||
"core": true,
|
||||
"sqlite": true,
|
||||
"patchright": true,
|
||||
"accounts": 0,
|
||||
"real_write_actions": 0
|
||||
}
|
||||
```
|
||||
|
||||
本次未启动真实业务窗口,因此没有触发登录、作品刷新、关注或私信操作;登录状态保持不变。
|
||||
@@ -0,0 +1,23 @@
|
||||
# CARD 信息布局与 Chrome 恢复页面提示
|
||||
|
||||
## 修改
|
||||
|
||||
- CARD 右侧信息顺序调整为:
|
||||
- `ID:xxx`
|
||||
- 作品描述,最多展示 2 行,完整内容保留在 tooltip
|
||||
- `点赞 · 评论 · 收藏 · 分享`
|
||||
- `发布时间:xxx`
|
||||
- `更新时间:xxx`
|
||||
- 封面改为左侧 132×158 全高展示,CARD 外层不再预留图片 padding。
|
||||
- 浏览器启动参数增加 `--disable-session-crashed-bubble`,禁止 Chromium 弹出“恢复页面/未正常关闭”提示;不修改登录目录中的 Cookie 或历史数据。
|
||||
|
||||
## 验证与部署
|
||||
|
||||
- 本地测试:56 个测试通过。
|
||||
- Windows 原生测试:56 个测试通过。
|
||||
- Windows x64 原生构建成功。
|
||||
- 安装前关闭了正在运行的 `DouyinAccounts.exe` 进程树。
|
||||
- 已静默更新到:`C:\Users\Rogee\AppData\Local\Programs\DouyinAccounts\DouyinAccounts.exe`
|
||||
- 安装后 dist 与已安装可执行文件 SHA256 一致:
|
||||
`6EEC024995F8EBE17CC71C007F5E6B40CC275...`
|
||||
- 安装包自带离线冒烟测试通过,Qt、核心、SQLite、Patchright 均正常,未执行真实业务写操作。
|
||||
@@ -0,0 +1,21 @@
|
||||
# CARD 互动事件分开展示
|
||||
|
||||
## 修改
|
||||
|
||||
作品卡片右侧互动数据由单行改为分行显示:
|
||||
|
||||
- `点赞:xxx`
|
||||
- `评论:xxx`
|
||||
- `收藏:xxx`
|
||||
- `分享:xxx`
|
||||
|
||||
描述仍支持最多两行折叠,并提供“展开/收起”按钮查看全文;封面随卡片高度保持左侧全高。
|
||||
|
||||
## Windows 部署验证
|
||||
|
||||
- Windows 原生单元测试:56 个通过。
|
||||
- Windows 原生构建成功。
|
||||
- 已静默安装到 `C:\Users\Rogee\AppData\Local\Programs\DouyinAccounts`。
|
||||
- 安装后的 dist 与已安装可执行文件 SHA256 一致:`5BC12B0D9584998785F9FA502E80060492D97...`
|
||||
- 安装包离线冒烟测试通过,未执行真实业务写操作。
|
||||
- 当前应用未运行。
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user