ClientInternal.h 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. #pragma once
  2. // Shared widgets/helpers used by Client*.cpp. Not part of the public Client API.
  3. #include "AvatarCache.h"
  4. #include "FileCache.h"
  5. #include "FileTypeIcon.h"
  6. #include "LocalStore.h"
  7. #include "UiHelpers.h"
  8. #include <functional>
  9. #include <QAction>
  10. #include <QApplication>
  11. #include <QCheckBox>
  12. #include <QColor>
  13. #include <QContextMenuEvent>
  14. #include <QDate>
  15. #include <QDateTime>
  16. #include <QDesktopServices>
  17. #include <QEvent>
  18. #include <QFile>
  19. #include <QFileDialog>
  20. #include <QFontMetrics>
  21. #include <QFrame>
  22. #include <QGridLayout>
  23. #include <QHBoxLayout>
  24. #include <QIcon>
  25. #include <QImage>
  26. #include <QLabel>
  27. #include <QListWidget>
  28. #include <QListWidgetItem>
  29. #include <QMenu>
  30. #include <QMouseEvent>
  31. #include <QObject>
  32. #include <QPaintEvent>
  33. #include <QPainter>
  34. #include <QPainterPath>
  35. #include <QPalette>
  36. #include <QPen>
  37. #include <QPixmap>
  38. #include <QPolygonF>
  39. #include <QProgressBar>
  40. #include <QPushButton>
  41. #include <QSizePolicy>
  42. #include <QTextLayout>
  43. #include <QTextOption>
  44. #include <QTimer>
  45. #include <QUrl>
  46. #include <QVBoxLayout>
  47. #include <QWidget>
  48. #include <QtMath>
  49. namespace ClientInternal {
  50. enum {
  51. RolePeerUid = Qt::UserRole,
  52. RoleName = Qt::UserRole + 1,
  53. RoleUnread = Qt::UserRole + 2,
  54. RolePreview = Qt::UserRole + 3,
  55. RoleTs = Qt::UserRole + 4,
  56. RoleAccount = Qt::UserRole + 5,
  57. RoleSignature = Qt::UserRole + 6,
  58. RoleRemark = Qt::UserRole + 7,
  59. RoleNick = Qt::UserRole + 8,
  60. RoleAvatarVer = Qt::UserRole + 9,
  61. RoleOnline = Qt::UserRole + 10
  62. };
  63. constexpr int kReconnectMaxTries = 3;
  64. constexpr int kReconnectIntervalSec = 10;
  65. inline QString fmtMs(qint64 nsecs)
  66. {
  67. return QString::number(nsecs / 1000000.0, 'f', 1);
  68. }
  69. class SpinRing : public QWidget
  70. {
  71. public:
  72. explicit SpinRing(QWidget *parent = nullptr)
  73. : QWidget(parent)
  74. {
  75. setFixedSize(36, 36);
  76. auto *t = new QTimer(this);
  77. connect(t, &QTimer::timeout, this, [this]() {
  78. m_angle = (m_angle + 30) % 360;
  79. update();
  80. });
  81. t->start(80);
  82. }
  83. protected:
  84. void paintEvent(QPaintEvent *) override
  85. {
  86. QPainter p(this);
  87. p.setRenderHint(QPainter::Antialiasing, true);
  88. const QRectF r = QRectF(rect()).adjusted(3.5, 3.5, -3.5, -3.5);
  89. QPen back(QColor(7, 193, 96, 40), 3, Qt::SolidLine, Qt::RoundCap);
  90. p.setPen(back);
  91. p.drawEllipse(r);
  92. QPen pen(QColor("#07C160"), 3, Qt::SolidLine, Qt::RoundCap);
  93. p.setPen(pen);
  94. p.drawArc(r, -m_angle * 16, 220 * 16);
  95. }
  96. private:
  97. int m_angle = 0;
  98. };
  99. struct Conv
  100. {
  101. qint64 uid = 0;
  102. QString name;
  103. QString preview;
  104. QString time;
  105. int unread = 0;
  106. int avatarVer = 0;
  107. bool online = false;
  108. };
  109. inline QString formatTs(qint64 ms)
  110. {
  111. if (ms <= 0)
  112. return QString();
  113. const QDateTime dt = QDateTime::fromMSecsSinceEpoch(ms);
  114. const QDate today = QDate::currentDate();
  115. if (dt.date() == today)
  116. return dt.toString(QStringLiteral("HH:mm"));
  117. if (dt.date() == today.addDays(-1))
  118. return QStringLiteral("昨天");
  119. if (dt.date().year() == today.year())
  120. return dt.toString(QStringLiteral("MM-dd"));
  121. return dt.toString(QStringLiteral("yyyy-MM-dd"));
  122. }
  123. class BubbleText : public QWidget
  124. {
  125. public:
  126. explicit BubbleText(const QString &text, QWidget *parent = nullptr)
  127. : QWidget(parent)
  128. , m_text(text)
  129. {
  130. setObjectName(QStringLiteral("chatText"));
  131. setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  132. setCursor(Qt::IBeamCursor);
  133. }
  134. QString text() const { return m_text; }
  135. void applyMaxWidth(int maxOuter)
  136. {
  137. const int ml = 10;
  138. const int mr = 10;
  139. const int mt = 8;
  140. const int mb = 8;
  141. const int innerMax = qMax(1, maxOuter - ml - mr);
  142. rebuild(innerMax);
  143. setFixedSize(m_size);
  144. if (QWidget *box = parentWidget())
  145. {
  146. if (auto *lay = qobject_cast<QVBoxLayout *>(box->layout()))
  147. lay->setContentsMargins(ml, mt, mr, mb);
  148. box->setFixedSize(m_size.width() + ml + mr, m_size.height() + mt + mb);
  149. }
  150. update();
  151. }
  152. protected:
  153. void paintEvent(QPaintEvent *) override
  154. {
  155. QPainter p(this);
  156. p.setRenderHint(QPainter::TextAntialiasing, true);
  157. p.setPen(QColor(QStringLiteral("#191919")));
  158. m_layout.draw(&p, QPointF(0, 0));
  159. }
  160. private:
  161. void rebuild(int innerMax)
  162. {
  163. m_layout.setText(m_text);
  164. m_layout.setFont(font());
  165. QTextOption opt;
  166. opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
  167. m_layout.setTextOption(opt);
  168. m_layout.setCacheEnabled(true);
  169. qreal y = 0;
  170. qreal maxLine = 0;
  171. m_layout.beginLayout();
  172. for (;;)
  173. {
  174. QTextLine line = m_layout.createLine();
  175. if (!line.isValid())
  176. break;
  177. line.setLineWidth(innerMax);
  178. line.setPosition(QPointF(0, y));
  179. y += line.height();
  180. maxLine = qMax(maxLine, line.naturalTextWidth());
  181. }
  182. m_layout.endLayout();
  183. if (y < 1)
  184. y = QFontMetrics(font()).height();
  185. m_size = QSize(qBound(1, qCeil(maxLine), innerMax), qCeil(y));
  186. }
  187. QString m_text;
  188. QTextLayout m_layout;
  189. QSize m_size;
  190. };
  191. inline void applyTextBubbleWidth(BubbleText *lab, int maxW)
  192. {
  193. if (lab)
  194. lab->applyMaxWidth(maxW);
  195. }
  196. class ClickWidget : public QFrame
  197. {
  198. public:
  199. std::function<void()> onClick;
  200. std::function<void()> onForward;
  201. std::function<void()> onSaveAs;
  202. std::function<void()> onFavorite;
  203. std::function<void()> onCopy;
  204. std::function<void()> onDelete;
  205. std::function<void()> onMultiSelect;
  206. explicit ClickWidget(QWidget *parent = nullptr) : QFrame(parent)
  207. {
  208. setCursor(Qt::PointingHandCursor);
  209. }
  210. protected:
  211. bool eventFilter(QObject *watched, QEvent *event) override
  212. {
  213. if (event->type() == QEvent::ContextMenu)
  214. {
  215. contextMenuEvent(static_cast<QContextMenuEvent *>(event));
  216. return true;
  217. }
  218. return QFrame::eventFilter(watched, event);
  219. }
  220. void mouseReleaseEvent(QMouseEvent *e) override
  221. {
  222. if (e->button() != Qt::LeftButton)
  223. return;
  224. QWidget *row = parentWidget();
  225. if (row && row->property("selecting").toBool())
  226. {
  227. if (auto *cb = row->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
  228. cb->toggle();
  229. return;
  230. }
  231. if (onClick)
  232. onClick();
  233. }
  234. void contextMenuEvent(QContextMenuEvent *e) override
  235. {
  236. QWidget *row = parentWidget();
  237. const bool selecting = row && row->property("selecting").toBool();
  238. if (!onCopy && !onForward && !onSaveAs && !onFavorite && !onDelete && !onMultiSelect)
  239. {
  240. QFrame::contextMenuEvent(e);
  241. return;
  242. }
  243. QMenu menu;
  244. QAction *copy = onCopy ? menu.addAction(QStringLiteral("复制")) : nullptr;
  245. QAction *fav = onFavorite ? menu.addAction(QStringLiteral("收藏")) : nullptr;
  246. QAction *fwd = onForward ? menu.addAction(QStringLiteral("转发")) : nullptr;
  247. QAction *save = onSaveAs ? menu.addAction(QStringLiteral("另存为")) : nullptr;
  248. if ((copy || fav || fwd || save) && (onDelete || onMultiSelect))
  249. menu.addSeparator();
  250. QAction *del = onDelete ? menu.addAction(QStringLiteral("删除")) : nullptr;
  251. QAction *multi = nullptr;
  252. if (onMultiSelect)
  253. multi = menu.addAction(selecting ? QStringLiteral("取消多选")
  254. : QStringLiteral("多选"));
  255. QAction *picked = menu.exec(e->globalPos());
  256. if (picked && picked == copy && onCopy)
  257. onCopy();
  258. else if (picked && picked == fav && onFavorite)
  259. onFavorite();
  260. else if (picked && picked == fwd && onForward)
  261. onForward();
  262. else if (picked && picked == save && onSaveAs)
  263. onSaveAs();
  264. else if (picked && picked == del && onDelete)
  265. onDelete();
  266. else if (picked && picked == multi && onMultiSelect)
  267. onMultiSelect();
  268. }
  269. };
  270. inline QPixmap fileCtlIcon(bool pause, bool upload = false)
  271. {
  272. const int size = 22;
  273. const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
  274. QPixmap pm(qRound(size * dpr), qRound(size * dpr));
  275. pm.fill(Qt::transparent);
  276. pm.setDevicePixelRatio(dpr);
  277. QPainter p(&pm);
  278. p.setRenderHint(QPainter::Antialiasing);
  279. const QColor fg(QStringLiteral("#576B95"));
  280. p.setPen(QPen(fg, 1.4));
  281. p.setBrush(Qt::NoBrush);
  282. p.drawEllipse(QRectF(1.2, 1.2, size - 2.4, size - 2.4));
  283. if (pause)
  284. {
  285. p.setPen(Qt::NoPen);
  286. p.setBrush(fg);
  287. p.drawRoundedRect(QRectF(7.2, 6.4, 2.6, 9.2), 0.8, 0.8);
  288. p.drawRoundedRect(QRectF(12.2, 6.4, 2.6, 9.2), 0.8, 0.8);
  289. }
  290. else
  291. {
  292. p.setPen(QPen(fg, 1.6, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
  293. if (upload)
  294. {
  295. p.drawLine(QPointF(11.0, 14.4), QPointF(11.0, 8.0));
  296. p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 6.4));
  297. p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 6.4));
  298. }
  299. else
  300. {
  301. p.drawLine(QPointF(11.0, 6.2), QPointF(11.0, 12.6));
  302. p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 14.4));
  303. p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 14.4));
  304. }
  305. }
  306. return pm;
  307. }
  308. inline QPushButton *fileCtlBtn(QWidget *parent, bool pause, bool upload = false)
  309. {
  310. auto *btn = new QPushButton(parent);
  311. btn->setFixedSize(22, 22);
  312. btn->setCursor(Qt::PointingHandCursor);
  313. btn->setFocusPolicy(Qt::NoFocus);
  314. btn->setFlat(true);
  315. btn->setIcon(QIcon(fileCtlIcon(pause, upload)));
  316. btn->setIconSize(QSize(22, 22));
  317. btn->setStyleSheet(QStringLiteral(
  318. "QPushButton { background:transparent; border:none; padding:0; }"
  319. "QPushButton:disabled { opacity:0.35; }"));
  320. return btn;
  321. }
  322. class FileBubble : public ClickWidget
  323. {
  324. public:
  325. FileBubble(QWidget *parent, qint64 fileId, const QString &name, qint64 size,
  326. const QString &url, const QString &mime, bool mine)
  327. : ClickWidget(parent)
  328. , m_fileId(fileId)
  329. , m_size(size)
  330. , m_url(url)
  331. , m_mime(mime)
  332. , m_name(name)
  333. , m_mine(mine)
  334. {
  335. setObjectName(QStringLiteral("fileBubble"));
  336. setStyleSheet(QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
  337. setFixedWidth(252);
  338. FileCache::instance().bindMeta(fileId, url, mime, name, size);
  339. auto *root = new QVBoxLayout(this);
  340. root->setContentsMargins(8, 8, 10, 8);
  341. root->setSpacing(6);
  342. auto *top = new QHBoxLayout();
  343. top->setContentsMargins(0, 0, 0, 0);
  344. top->setSpacing(8);
  345. auto *icon = new QLabel(this);
  346. icon->setFixedSize(40, 40);
  347. icon->setScaledContents(true);
  348. icon->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  349. if (FileCache::looksLikeImage(mime, name))
  350. {
  351. const QPixmap pm = FileCache::instance().thumbnail(fileId, 40);
  352. icon->setPixmap(pm.isNull() ? FileTypeIcon::pixmap(name, 40) : pm);
  353. }
  354. else
  355. {
  356. icon->setPixmap(FileTypeIcon::pixmap(name, 40));
  357. }
  358. auto *info = new QVBoxLayout();
  359. info->setContentsMargins(0, 0, 0, 0);
  360. info->setSpacing(2);
  361. auto *n = new QLabel(name, this);
  362. n->setWordWrap(true);
  363. n->setStyleSheet(QStringLiteral(
  364. "color:#191919; font-size:13px; background:transparent; border:none;"));
  365. m_statusLab = new QLabel(this);
  366. m_statusLab->setStyleSheet(QStringLiteral(
  367. "color:#888888; font-size:11px; background:transparent; border:none;"));
  368. info->addWidget(n);
  369. info->addWidget(m_statusLab);
  370. info->addStretch();
  371. top->addWidget(icon, 0, Qt::AlignTop);
  372. top->addLayout(info, 1);
  373. m_bar = new QProgressBar(this);
  374. m_bar->setRange(0, 1000);
  375. m_bar->setValue(0);
  376. m_bar->setTextVisible(false);
  377. m_bar->setToolTip(QString());
  378. m_bar->setFixedHeight(6);
  379. m_bar->setStyleSheet(QStringLiteral(
  380. "QProgressBar { background:rgba(0,0,0,0.08); border:none; border-radius:3px; }"
  381. "QProgressBar::chunk { background:#07C160; border-radius:3px; }"));
  382. auto *meta = new QHBoxLayout();
  383. meta->setContentsMargins(0, 0, 0, 0);
  384. meta->setSpacing(8);
  385. m_sizeLab = new QLabel(this);
  386. m_sizeLab->setStyleSheet(QStringLiteral(
  387. "color:#888888; font-size:11px; background:transparent; border:none;"));
  388. m_speedLab = new QLabel(this);
  389. m_speedLab->setStyleSheet(QStringLiteral(
  390. "color:#888888; font-size:11px; background:transparent; border:none;"));
  391. meta->addWidget(m_sizeLab, 1);
  392. meta->addWidget(m_speedLab, 0, Qt::AlignRight);
  393. root->addLayout(top);
  394. root->addWidget(m_bar);
  395. root->addLayout(meta);
  396. m_bar->hide();
  397. m_sizeLab->hide();
  398. m_speedLab->hide();
  399. connect(&FileCache::instance(), &FileCache::statusChanged, this, [this](qint64 id) {
  400. if (id == m_fileId)
  401. syncUi();
  402. });
  403. connect(&FileCache::instance(), &FileCache::progress, this,
  404. [this](qint64 id, qint64, qint64, double) {
  405. if (id == m_fileId)
  406. syncUi();
  407. });
  408. connect(&FileCache::instance(), &FileCache::failed, this,
  409. [this](qint64 id, const QString &error) {
  410. if (id != m_fileId)
  411. return;
  412. const bool uploading = FileCache::instance().needsUpload(m_fileId)
  413. || FileCache::instance().status(m_fileId) == FileCache::Uploading;
  414. if (!uploading)
  415. {
  416. UiHelpers::showToast(window(),
  417. error.isEmpty() ? QStringLiteral("下载失败") : error,
  418. true);
  419. }
  420. syncUi();
  421. });
  422. onClick = [this]() {
  423. const QString path = FileCache::instance().localPath(m_fileId);
  424. if (!path.isEmpty())
  425. {
  426. if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path)))
  427. UiHelpers::showToast(window(), QStringLiteral("无法打开文件"), true);
  428. return;
  429. }
  430. if (FileCache::instance().needsUpload(m_fileId))
  431. {
  432. FileCache::instance().startUpload(m_fileId);
  433. return;
  434. }
  435. m_wantReceive = true;
  436. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  437. syncUi();
  438. };
  439. onSaveAs = [this]() {
  440. const QString src = FileCache::instance().localPath(m_fileId);
  441. if (src.isEmpty())
  442. {
  443. UiHelpers::showToast(window(), QStringLiteral("请先下载文件"), true);
  444. return;
  445. }
  446. const QString dest = QFileDialog::getSaveFileName(window(), QStringLiteral("保存文件"), m_name);
  447. if (dest.isEmpty())
  448. return;
  449. QFile::remove(dest);
  450. if (!QFile::copy(src, dest))
  451. UiHelpers::showToast(window(), QStringLiteral("保存失败"), true);
  452. else
  453. UiHelpers::showToast(window(), QStringLiteral("已保存"));
  454. };
  455. syncUi();
  456. }
  457. void bindActionSlot(QWidget *slot, QLabel *sendMark)
  458. {
  459. m_actionSlot = slot;
  460. m_sendMark = sendMark;
  461. if (!m_actionSlot)
  462. return;
  463. m_dl = fileCtlBtn(m_actionSlot, false, false);
  464. m_pause = fileCtlBtn(m_actionSlot, true);
  465. m_dl->setParent(m_actionSlot);
  466. m_pause->setParent(m_actionSlot);
  467. m_dl->move(0, 0);
  468. m_pause->move(0, 0);
  469. m_dl->raise();
  470. m_pause->raise();
  471. connect(m_dl, &QPushButton::clicked, this, [this]() {
  472. if (FileCache::instance().needsUpload(m_fileId))
  473. FileCache::instance().startUpload(m_fileId);
  474. else
  475. {
  476. m_wantReceive = true;
  477. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  478. }
  479. });
  480. connect(m_pause, &QPushButton::clicked, this, [this]() {
  481. if (FileCache::instance().needsUpload(m_fileId)
  482. || FileCache::instance().status(m_fileId) == FileCache::Uploading)
  483. FileCache::instance().cancelUpload(m_fileId);
  484. else
  485. FileCache::instance().cancelDownload(m_fileId);
  486. });
  487. syncUi();
  488. }
  489. void refreshActionSlot()
  490. {
  491. syncUi();
  492. }
  493. private:
  494. void syncUi()
  495. {
  496. const FileCache::Status st = FileCache::instance().status(m_fileId);
  497. const bool pendingUp = m_mine && FileCache::instance().needsUpload(m_fileId);
  498. qint64 rec = 0, tot = m_size;
  499. double bps = 0;
  500. FileCache::instance().progressOf(m_fileId, &rec, &tot, &bps);
  501. if (tot <= 0)
  502. tot = m_size;
  503. const bool received = (st == FileCache::Received) && !pendingUp;
  504. const bool transferring = (st == FileCache::Downloading) || (st == FileCache::Uploading);
  505. const bool transferStarted = transferring || (rec > 0 && !received);
  506. const bool sentOnServer = m_mine && !pendingUp && m_fileId > 0;
  507. const bool showAsSent = sentOnServer && !m_wantReceive && !transferStarted;
  508. QString status = FileCache::statusText(st);
  509. if (pendingUp && st != FileCache::Uploading)
  510. status = QStringLiteral("已暂停");
  511. else if (st == FileCache::NotReceived && rec > 0)
  512. status = QStringLiteral("已暂停");
  513. if (showAsSent)
  514. status = QStringLiteral("已发送");
  515. m_statusLab->setText(status);
  516. if (showAsSent)
  517. {
  518. m_bar->hide();
  519. m_sizeLab->hide();
  520. m_speedLab->hide();
  521. }
  522. else if (transferStarted)
  523. {
  524. m_bar->show();
  525. m_sizeLab->show();
  526. m_speedLab->show();
  527. if (tot > 0)
  528. m_bar->setValue(
  529. static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
  530. else
  531. m_bar->setValue(0);
  532. m_sizeLab->setText(QStringLiteral("%1 / %2")
  533. .arg(FileCache::formatSize(rec),
  534. tot > 0 ? FileCache::formatSize(tot)
  535. : QStringLiteral("--")));
  536. if (transferring)
  537. m_speedLab->setText(FileCache::formatSpeed(bps));
  538. else
  539. m_speedLab->setText(QStringLiteral("已暂停"));
  540. }
  541. else
  542. {
  543. m_bar->hide();
  544. m_sizeLab->hide();
  545. m_speedLab->hide();
  546. }
  547. if (!m_actionSlot || !m_dl || !m_pause)
  548. return;
  549. const int sendState = parentWidget()
  550. ? parentWidget()->property("sendStatus").toInt()
  551. : LocalMsg::Ok;
  552. const bool sendBusy = m_mine
  553. && (sendState == LocalMsg::Sending || sendState == LocalMsg::Failed);
  554. if (sendBusy)
  555. {
  556. m_dl->hide();
  557. m_pause->hide();
  558. if (m_actionSlot)
  559. m_actionSlot->show();
  560. if (m_sendMark)
  561. m_sendMark->show();
  562. return;
  563. }
  564. if (received || showAsSent)
  565. {
  566. m_dl->hide();
  567. m_pause->hide();
  568. if (m_sendMark)
  569. m_sendMark->hide();
  570. if (m_actionSlot)
  571. m_actionSlot->hide();
  572. return;
  573. }
  574. if (m_actionSlot)
  575. m_actionSlot->show();
  576. if (m_sendMark)
  577. m_sendMark->hide();
  578. m_dl->setIcon(QIcon(fileCtlIcon(false, pendingUp)));
  579. m_dl->setVisible(!transferring);
  580. m_pause->setVisible(transferring);
  581. }
  582. qint64 m_fileId = 0;
  583. qint64 m_size = 0;
  584. QString m_url;
  585. QString m_mime;
  586. QString m_name;
  587. bool m_mine = false;
  588. bool m_wantReceive = false;
  589. QWidget *m_actionSlot = nullptr;
  590. QLabel *m_sendMark = nullptr;
  591. QPushButton *m_dl = nullptr;
  592. QPushButton *m_pause = nullptr;
  593. QLabel *m_statusLab = nullptr;
  594. QProgressBar *m_bar = nullptr;
  595. QLabel *m_sizeLab = nullptr;
  596. QLabel *m_speedLab = nullptr;
  597. };
  598. inline void refreshRowFileAction(QWidget *row)
  599. {
  600. if (!row)
  601. return;
  602. if (auto *fb = row->findChild<FileBubble *>(QStringLiteral("fileBubble")))
  603. fb->refreshActionSlot();
  604. }
  605. using ElideLabel = UiHelpers::ElideLabel;
  606. class TrayPreviewRow : public QWidget
  607. {
  608. public:
  609. std::function<void()> onActivate;
  610. explicit TrayPreviewRow(QWidget *parent = nullptr)
  611. : QWidget(parent)
  612. {
  613. setAttribute(Qt::WA_Hover);
  614. setCursor(Qt::PointingHandCursor);
  615. setAutoFillBackground(true);
  616. setMinimumHeight(48);
  617. applyHover(false);
  618. }
  619. protected:
  620. void enterEvent(QEvent *event) override
  621. {
  622. applyHover(true);
  623. QWidget::enterEvent(event);
  624. }
  625. void leaveEvent(QEvent *event) override
  626. {
  627. applyHover(false);
  628. QWidget::leaveEvent(event);
  629. }
  630. void mouseReleaseEvent(QMouseEvent *event) override
  631. {
  632. if (event->button() == Qt::LeftButton && rect().contains(event->pos()) && onActivate)
  633. onActivate();
  634. QWidget::mouseReleaseEvent(event);
  635. }
  636. private:
  637. void applyHover(bool on)
  638. {
  639. QPalette pal = palette();
  640. pal.setColor(QPalette::Window, on ? QColor("#F2F2F2") : QColor("#FFFFFF"));
  641. setPalette(pal);
  642. }
  643. };
  644. inline QPixmap navIcon(int kind, const QColor &color)
  645. {
  646. const int size = 22;
  647. QPixmap pm(size, size);
  648. pm.fill(Qt::transparent);
  649. QPainter p(&pm);
  650. p.setRenderHint(QPainter::Antialiasing, true);
  651. p.setPen(QPen(color, 1.6));
  652. p.setBrush(Qt::NoBrush);
  653. if (kind == 0)
  654. {
  655. p.drawRoundedRect(QRectF(2.5, 3.5, 17, 12.5), 2.5, 2.5);
  656. QPainterPath tail;
  657. tail.moveTo(6.5, 16);
  658. tail.lineTo(8.5, 20);
  659. tail.lineTo(12, 16);
  660. p.setBrush(color);
  661. p.setPen(Qt::NoPen);
  662. p.drawPath(tail);
  663. }
  664. else if (kind == 1)
  665. {
  666. p.drawEllipse(QRectF(6.5, 3.2, 9, 9));
  667. p.drawArc(QRect(3, 12, 16, 12), 20 * 16, 140 * 16);
  668. }
  669. else if (kind == 3)
  670. {
  671. QPolygonF star;
  672. for (int i = 0; i < 5; ++i)
  673. {
  674. const qreal a = -1.5707963 + i * 1.25663706;
  675. star << QPointF(11 + qCos(a) * 8.2, 11 + qSin(a) * 8.2);
  676. const qreal b = a + 0.62831853;
  677. star << QPointF(11 + qCos(b) * 3.4, 11 + qSin(b) * 3.4);
  678. }
  679. p.setBrush(color);
  680. p.setPen(Qt::NoPen);
  681. p.drawPolygon(star);
  682. }
  683. else
  684. {
  685. const QPointF c(11, 11);
  686. for (int i = 0; i < 6; ++i)
  687. {
  688. const qreal a = i * 3.14159265 / 3.0;
  689. const QPointF d(qCos(a), qSin(a));
  690. p.drawLine(c + d * 5.2, c + d * 8.6);
  691. }
  692. p.drawEllipse(QRectF(6.6, 6.6, 8.8, 8.8));
  693. p.drawEllipse(QRectF(8.6, 8.6, 4.8, 4.8));
  694. }
  695. return pm;
  696. }
  697. inline QPixmap chatToolIcon(int kind, const QColor &color, int size = 20)
  698. {
  699. const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
  700. QPixmap pm(qRound(size * dpr), qRound(size * dpr));
  701. pm.fill(Qt::transparent);
  702. pm.setDevicePixelRatio(dpr);
  703. QPainter p(&pm);
  704. p.setRenderHint(QPainter::Antialiasing, true);
  705. p.setPen(QPen(color, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
  706. p.setBrush(Qt::NoBrush);
  707. const qreal s = size;
  708. if (kind == 0)
  709. {
  710. p.drawEllipse(QRectF(2.2, 3.2, s - 4.4, s - 5.2));
  711. p.drawArc(QRectF(5.5, 7.5, s - 11.0, s - 12.0), 200 * 16, 140 * 16);
  712. p.setBrush(color);
  713. p.setPen(Qt::NoPen);
  714. p.drawEllipse(QRectF(6.4, 7.2, 2.2, 2.2));
  715. p.drawEllipse(QRectF(s - 8.6, 7.2, 2.2, 2.2));
  716. }
  717. else if (kind == 1)
  718. {
  719. p.drawRoundedRect(QRectF(2.5, 4.2, s - 5.0, s - 8.0), 2.0, 2.0);
  720. p.setBrush(color);
  721. p.setPen(Qt::NoPen);
  722. p.drawEllipse(QRectF(5.2, 6.4, 3.0, 3.0));
  723. QPolygonF hill;
  724. hill << QPointF(3.6, s - 5.2) << QPointF(8.2, 10.2) << QPointF(11.2, 13.0)
  725. << QPointF(14.4, 9.4) << QPointF(s - 3.6, s - 5.2);
  726. p.drawPolygon(hill);
  727. }
  728. else
  729. {
  730. p.drawRoundedRect(QRectF(5.0, 2.8, s - 9.2, s - 5.4), 1.2, 1.2);
  731. p.drawLine(QPointF(5.0, 6.4), QPointF(s - 4.2, 6.4));
  732. p.drawLine(QPointF(7.4, 9.4), QPointF(s - 7.2, 9.4));
  733. p.drawLine(QPointF(7.4, 12.2), QPointF(s - 7.2, 12.2));
  734. }
  735. return pm;
  736. }
  737. inline QPixmap settingsNavIcon()
  738. {
  739. static QPixmap cached;
  740. if (!cached.isNull())
  741. return cached;
  742. QPixmap src(QStringLiteral(":/icons/settings.png"));
  743. if (src.isNull())
  744. {
  745. cached = navIcon(2, QColor("#A6A6A6"));
  746. return cached;
  747. }
  748. QImage img = src.toImage().convertToFormat(QImage::Format_ARGB32);
  749. for (int y = 0; y < img.height(); ++y)
  750. {
  751. auto *line = reinterpret_cast<QRgb *>(img.scanLine(y));
  752. for (int x = 0; x < img.width(); ++x)
  753. {
  754. if (qRed(line[x]) < 40 && qGreen(line[x]) < 40 && qBlue(line[x]) < 40)
  755. line[x] = qRgba(0, 0, 0, 0);
  756. }
  757. }
  758. cached = QPixmap::fromImage(img).scaled(22, 22, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  759. return cached;
  760. }
  761. inline QPushButton *navBtn(int kind, const QString &tip, QWidget *parent, bool checkable = true)
  762. {
  763. auto *btn = new QPushButton(parent);
  764. btn->setCheckable(checkable);
  765. btn->setFixedSize(56, 48);
  766. btn->setCursor(Qt::PointingHandCursor);
  767. btn->setIconSize(QSize(22, 22));
  768. btn->setIcon(QIcon(kind == 2 ? settingsNavIcon() : navIcon(kind, QColor("#A6A6A6"))));
  769. btn->setStyleSheet(QStringLiteral(
  770. "QPushButton { background:transparent; border:none; border-radius:0; padding:0; }"
  771. "QPushButton:hover { background:#3A3A3A; }"
  772. "QPushButton:checked { background:transparent; }"));
  773. if (checkable)
  774. {
  775. QObject::connect(btn, &QPushButton::toggled, btn, [btn, kind](bool on) {
  776. btn->setIcon(QIcon(navIcon(kind, on ? QColor("#07C160") : QColor("#A6A6A6"))));
  777. });
  778. }
  779. return btn;
  780. }
  781. inline QLabel *makeOnlineDot(QWidget *parent, bool online)
  782. {
  783. auto *dot = new QLabel(parent);
  784. dot->setFixedSize(8, 8);
  785. dot->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  786. dot->setAttribute(Qt::WA_StyledBackground, true);
  787. dot->setStyleSheet(online
  788. ? QStringLiteral("background:#07C160; border:none; border-radius:4px;")
  789. : QStringLiteral("background:#C8C8C8; border:none; border-radius:4px;"));
  790. return dot;
  791. }
  792. inline Conv convFromItem(const QListWidgetItem *item)
  793. {
  794. Conv c;
  795. c.uid = item->data(RolePeerUid).toLongLong();
  796. c.name = item->data(RoleName).toString();
  797. c.preview = item->data(RolePreview).toString();
  798. c.time = formatTs(item->data(RoleTs).toLongLong());
  799. c.unread = item->data(RoleUnread).toInt();
  800. c.avatarVer = item->data(RoleAvatarVer).toInt();
  801. c.online = item->data(RoleOnline).toInt() == 1;
  802. return c;
  803. }
  804. inline bool listOnlineOf(const QListWidget *list, qint64 uid)
  805. {
  806. if (!list)
  807. return false;
  808. for (int i = 0; i < list->count(); ++i)
  809. {
  810. auto *item = list->item(i);
  811. if (item && item->data(RolePeerUid).toLongLong() == uid)
  812. return item->data(RoleOnline).toInt() == 1;
  813. }
  814. return false;
  815. }
  816. inline QIcon blankTrayIcon()
  817. {
  818. QPixmap pm(32, 32);
  819. pm.fill(Qt::transparent);
  820. return QIcon(pm);
  821. }
  822. inline QWidget *makeFriendRow(const QString &name, bool online, qint64 uid, int avatarVer, QWidget *parent)
  823. {
  824. auto *row = new QWidget(parent);
  825. row->setFixedHeight(48);
  826. row->setMinimumWidth(0);
  827. row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
  828. row->setAttribute(Qt::WA_TranslucentBackground);
  829. row->setStyleSheet(QStringLiteral("background:transparent;"));
  830. auto *lay = new QHBoxLayout(row);
  831. lay->setContentsMargins(12, 8, 12, 8);
  832. lay->setSpacing(10);
  833. auto *avatar = new QLabel(row);
  834. avatar->setFixedSize(32, 32);
  835. avatar->setPixmap(AvatarCache::instance().pixmap(uid, avatarVer, name, 32));
  836. auto *nameLab = new ElideLabel(name, row);
  837. nameLab->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
  838. lay->addWidget(avatar, 0, Qt::AlignVCenter);
  839. lay->addWidget(nameLab, 1, Qt::AlignVCenter);
  840. if (online)
  841. lay->addWidget(makeOnlineDot(row, true), 0, Qt::AlignVCenter);
  842. return row;
  843. }
  844. inline QWidget *makeConvRow(const Conv &c, QWidget *parent)
  845. {
  846. auto *row = new QWidget(parent);
  847. row->setFixedHeight(68);
  848. row->setMinimumWidth(0);
  849. row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
  850. row->setAttribute(Qt::WA_TranslucentBackground);
  851. row->setStyleSheet(QStringLiteral("background:transparent;"));
  852. auto *grid = new QGridLayout(row);
  853. grid->setContentsMargins(12, 10, 12, 10);
  854. grid->setHorizontalSpacing(10);
  855. grid->setVerticalSpacing(3);
  856. grid->setColumnStretch(1, 1);
  857. grid->setColumnMinimumWidth(1, 0);
  858. auto *avatarBox = new QWidget(row);
  859. avatarBox->setFixedSize(44, 44);
  860. auto *avatar = new QLabel(avatarBox);
  861. avatar->setGeometry(0, 0, 40, 40);
  862. avatar->setPixmap(AvatarCache::instance().pixmap(c.uid, c.avatarVer, c.name, 40));
  863. avatar->setScaledContents(false);
  864. if (c.online)
  865. {
  866. auto *dot = makeOnlineDot(avatarBox, true);
  867. dot->setFixedSize(10, 10);
  868. dot->move(30, 30);
  869. dot->setStyleSheet(QStringLiteral(
  870. "background:#07C160; border:2px solid #FFFFFF; border-radius:5px;"));
  871. dot->raise();
  872. }
  873. if (c.unread > 0)
  874. {
  875. auto *badge = new QLabel(c.unread > 99 ? QStringLiteral("99") : QString::number(c.unread), avatarBox);
  876. badge->setObjectName(QStringLiteral("unreadBadge"));
  877. badge->setAlignment(Qt::AlignCenter);
  878. badge->setFixedSize(16, 16);
  879. badge->move(28, 0);
  880. badge->setStyleSheet(QStringLiteral(
  881. "background:#FA5151; color:#FFFFFF; font-size:10px; border:none; border-radius:8px;"));
  882. badge->raise();
  883. }
  884. auto *nameRow = new QWidget(row);
  885. nameRow->setMinimumWidth(0);
  886. nameRow->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
  887. nameRow->setStyleSheet(QStringLiteral("background:transparent;"));
  888. auto *nameLay = new QHBoxLayout(nameRow);
  889. nameLay->setContentsMargins(0, 0, 0, 0);
  890. nameLay->setSpacing(6);
  891. auto *name = new ElideLabel(c.name, nameRow);
  892. name->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
  893. nameLay->addWidget(name, 1, Qt::AlignVCenter);
  894. auto *prevWrap = new QWidget(row);
  895. prevWrap->setMinimumWidth(0);
  896. prevWrap->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
  897. prevWrap->setStyleSheet(QStringLiteral("background:transparent;"));
  898. auto *prevLay = new QHBoxLayout(prevWrap);
  899. prevLay->setContentsMargins(0, 0, 0, 0);
  900. prevLay->setSpacing(4);
  901. QString previewText = c.preview;
  902. int iconKind = -1;
  903. if (previewText.startsWith(QStringLiteral("[表情]")))
  904. {
  905. iconKind = 0;
  906. previewText = previewText.mid(4);
  907. if (previewText.isEmpty())
  908. previewText = QStringLiteral("表情");
  909. }
  910. else if (previewText.startsWith(QStringLiteral("[图片]")))
  911. {
  912. iconKind = 1;
  913. previewText = previewText.mid(4);
  914. if (previewText.isEmpty())
  915. previewText = QStringLiteral("图片");
  916. }
  917. else if (previewText.startsWith(QStringLiteral("[文件]")))
  918. {
  919. iconKind = 2;
  920. previewText = previewText.mid(4).trimmed();
  921. if (previewText.isEmpty())
  922. previewText = QStringLiteral("文件");
  923. }
  924. if (iconKind >= 0)
  925. {
  926. auto *ic = new QLabel(prevWrap);
  927. ic->setFixedSize(14, 14);
  928. ic->setPixmap(chatToolIcon(iconKind, QColor("#9A9A9A"), 14));
  929. ic->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  930. prevLay->addWidget(ic, 0, Qt::AlignVCenter);
  931. }
  932. auto *preview = new ElideLabel(previewText, prevWrap);
  933. preview->setStyleSheet(QStringLiteral("color:#9A9A9A; font-size:12px; background:transparent;"));
  934. prevLay->addWidget(preview, 1);
  935. auto *time = new QLabel(c.time, row);
  936. time->setStyleSheet(QStringLiteral("color:#B2B2B2; font-size:11px; background:transparent;"));
  937. time->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
  938. time->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
  939. time->setMinimumWidth(time->fontMetrics().horizontalAdvance(
  940. c.time.isEmpty() ? QStringLiteral("00:00") : c.time));
  941. grid->addWidget(avatarBox, 0, 0, 2, 1, Qt::AlignVCenter);
  942. grid->addWidget(nameRow, 0, 1);
  943. grid->addWidget(time, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
  944. grid->addWidget(prevWrap, 1, 1, 1, 2);
  945. return row;
  946. }
  947. } // namespace ClientInternal