ClientInternal.h 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600
  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 "common/protocol_def.h"
  9. #include <functional>
  10. #include <QAction>
  11. #include <QApplication>
  12. #include <QCheckBox>
  13. #include <QColor>
  14. #include <QContextMenuEvent>
  15. #include <QDate>
  16. #include <QDateTime>
  17. #include <QDesktopServices>
  18. #include <QEvent>
  19. #include <QFile>
  20. #include <QFileDialog>
  21. #include <QFont>
  22. #include <QFontMetrics>
  23. #include <QFrame>
  24. #include <QGridLayout>
  25. #include <QHBoxLayout>
  26. #include <QHash>
  27. #include <QIcon>
  28. #include <QImage>
  29. #include <QLabel>
  30. #include <QListWidget>
  31. #include <QListWidgetItem>
  32. #include <QMenu>
  33. #include <QMouseEvent>
  34. #include <QObject>
  35. #include <QPaintEvent>
  36. #include <QPainter>
  37. #include <QPainterPath>
  38. #include <QPalette>
  39. #include <QPen>
  40. #include <QPixmap>
  41. #include <QPolygonF>
  42. #include <QPointer>
  43. #include <QProgressBar>
  44. #include <QPushButton>
  45. #include <QResizeEvent>
  46. #include <QScrollArea>
  47. #include <QScrollBar>
  48. #include <QSizePolicy>
  49. #include <QTextLayout>
  50. #include <QTextOption>
  51. #include <QTimer>
  52. #include <QUrl>
  53. #include <QVBoxLayout>
  54. #include <QWidget>
  55. #include <QtMath>
  56. namespace ClientInternal {
  57. enum {
  58. RolePeerUid = Qt::UserRole,
  59. RoleName = Qt::UserRole + 1,
  60. RoleUnread = Qt::UserRole + 2,
  61. RolePreview = Qt::UserRole + 3,
  62. RoleTs = Qt::UserRole + 4,
  63. RoleAccount = Qt::UserRole + 5,
  64. RoleSignature = Qt::UserRole + 6,
  65. RoleRemark = Qt::UserRole + 7,
  66. RoleNick = Qt::UserRole + 8,
  67. RoleAvatarVer = Qt::UserRole + 9,
  68. RoleOnline = Qt::UserRole + 10,
  69. RoleConvType = Qt::UserRole + 11,
  70. RoleMemberCount = Qt::UserRole + 12,
  71. RoleJoinMode = Qt::UserRole + 13
  72. };
  73. constexpr int ConvDm = im::ConvType::Direct;
  74. constexpr int ConvGroup = im::ConvType::Group;
  75. struct ConvId
  76. {
  77. int type = ConvDm;
  78. qint64 id = 0;
  79. bool isGroup() const { return type == ConvGroup; }
  80. bool isValid() const { return id > 0; }
  81. bool operator==(const ConvId &o) const { return type == o.type && id == o.id; }
  82. bool operator!=(const ConvId &o) const { return !(*this == o); }
  83. };
  84. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  85. inline uint qHash(const ConvId &c, uint seed = 0)
  86. {
  87. return ::qHash(static_cast<uint>(c.type), seed) ^ ::qHash(static_cast<quint64>(c.id));
  88. }
  89. #else
  90. inline size_t qHash(const ConvId &c, size_t seed = 0)
  91. {
  92. return ::qHash(static_cast<uint>(c.type), seed) ^ ::qHash(static_cast<quint64>(c.id), seed);
  93. }
  94. #endif
  95. inline QPixmap groupIcon(int size)
  96. {
  97. return UiHelpers::groupIconPixmap(size);
  98. }
  99. inline QString groupTitle(const QString &name, int memberCount)
  100. {
  101. if (memberCount > 0)
  102. return name + QStringLiteral(" (%1)").arg(memberCount);
  103. return name;
  104. }
  105. constexpr int kReconnectMaxTries = 3;
  106. constexpr int kReconnectIntervalSec = 10;
  107. inline QString fmtMs(qint64 nsecs)
  108. {
  109. return QString::number(nsecs / 1000000.0, 'f', 1);
  110. }
  111. class SpinRing : public QWidget
  112. {
  113. public:
  114. explicit SpinRing(QWidget *parent = nullptr)
  115. : QWidget(parent)
  116. {
  117. setFixedSize(36, 36);
  118. auto *t = new QTimer(this);
  119. connect(t, &QTimer::timeout, this, [this]() {
  120. m_angle = (m_angle + 30) % 360;
  121. update();
  122. });
  123. t->start(80);
  124. }
  125. protected:
  126. void paintEvent(QPaintEvent *) override
  127. {
  128. QPainter p(this);
  129. p.setRenderHint(QPainter::Antialiasing, true);
  130. const QRectF r = QRectF(rect()).adjusted(3.5, 3.5, -3.5, -3.5);
  131. QPen back(QColor(7, 193, 96, 40), 3, Qt::SolidLine, Qt::RoundCap);
  132. p.setPen(back);
  133. p.drawEllipse(r);
  134. QPen pen(QColor("#07C160"), 3, Qt::SolidLine, Qt::RoundCap);
  135. p.setPen(pen);
  136. p.drawArc(r, -m_angle * 16, 220 * 16);
  137. }
  138. private:
  139. int m_angle = 0;
  140. };
  141. struct Conv
  142. {
  143. qint64 uid = 0;
  144. int convType = ConvDm;
  145. QString name;
  146. QString preview;
  147. QString time;
  148. int unread = 0;
  149. int avatarVer = 0;
  150. bool online = false;
  151. int memberCount = 0;
  152. };
  153. inline QString formatTs(qint64 ms)
  154. {
  155. if (ms <= 0)
  156. return QString();
  157. const QDateTime dt = QDateTime::fromMSecsSinceEpoch(ms);
  158. const QDate today = QDate::currentDate();
  159. if (dt.date() == today)
  160. return dt.toString(QStringLiteral("HH:mm"));
  161. if (dt.date() == today.addDays(-1))
  162. return QStringLiteral("昨天");
  163. if (dt.date().year() == today.year())
  164. return dt.toString(QStringLiteral("MM-dd"));
  165. return dt.toString(QStringLiteral("yyyy-MM-dd"));
  166. }
  167. class BubbleText : public QWidget
  168. {
  169. public:
  170. explicit BubbleText(const QString &text, QWidget *parent = nullptr)
  171. : QWidget(parent)
  172. , m_text(text)
  173. {
  174. setObjectName(QStringLiteral("chatText"));
  175. setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  176. setCursor(Qt::IBeamCursor);
  177. }
  178. QString text() const { return m_text; }
  179. void applyMaxWidth(int maxOuter)
  180. {
  181. const int ml = 10;
  182. const int mr = 10;
  183. const int mt = 8;
  184. const int mb = 8;
  185. const int innerMax = qMax(1, maxOuter - ml - mr);
  186. rebuild(innerMax);
  187. setFixedSize(m_size);
  188. if (QWidget *box = parentWidget())
  189. {
  190. if (auto *lay = qobject_cast<QVBoxLayout *>(box->layout()))
  191. lay->setContentsMargins(ml, mt, mr, mb);
  192. box->setFixedSize(m_size.width() + ml + mr, m_size.height() + mt + mb);
  193. }
  194. update();
  195. }
  196. protected:
  197. void paintEvent(QPaintEvent *) override
  198. {
  199. QPainter p(this);
  200. p.setRenderHint(QPainter::TextAntialiasing, true);
  201. p.setPen(QColor(QStringLiteral("#191919")));
  202. m_layout.draw(&p, QPointF(0, 0));
  203. }
  204. private:
  205. void rebuild(int innerMax)
  206. {
  207. m_layout.setText(m_text);
  208. m_layout.setFont(font());
  209. QTextOption opt;
  210. opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
  211. m_layout.setTextOption(opt);
  212. m_layout.setCacheEnabled(true);
  213. qreal y = 0;
  214. qreal maxLine = 0;
  215. m_layout.beginLayout();
  216. for (;;)
  217. {
  218. QTextLine line = m_layout.createLine();
  219. if (!line.isValid())
  220. break;
  221. line.setLineWidth(innerMax);
  222. line.setPosition(QPointF(0, y));
  223. y += line.height();
  224. maxLine = qMax(maxLine, line.naturalTextWidth());
  225. }
  226. m_layout.endLayout();
  227. if (y < 1)
  228. y = QFontMetrics(font()).height();
  229. m_size = QSize(qBound(1, qCeil(maxLine), innerMax), qCeil(y));
  230. }
  231. QString m_text;
  232. QTextLayout m_layout;
  233. QSize m_size;
  234. };
  235. inline void applyTextBubbleWidth(BubbleText *lab, int maxW)
  236. {
  237. if (lab)
  238. lab->applyMaxWidth(maxW);
  239. }
  240. class ClickWidget : public QFrame
  241. {
  242. public:
  243. std::function<void()> onClick;
  244. std::function<void()> onForward;
  245. std::function<void()> onSaveAs;
  246. std::function<void()> onFavorite;
  247. std::function<void()> onCopy;
  248. std::function<void()> onDelete;
  249. std::function<void()> onMultiSelect;
  250. explicit ClickWidget(QWidget *parent = nullptr) : QFrame(parent)
  251. {
  252. setCursor(Qt::PointingHandCursor);
  253. }
  254. protected:
  255. bool eventFilter(QObject *watched, QEvent *event) override
  256. {
  257. if (event->type() == QEvent::ContextMenu)
  258. {
  259. contextMenuEvent(static_cast<QContextMenuEvent *>(event));
  260. return true;
  261. }
  262. return QFrame::eventFilter(watched, event);
  263. }
  264. void mouseReleaseEvent(QMouseEvent *e) override
  265. {
  266. if (e->button() != Qt::LeftButton)
  267. return;
  268. QWidget *row = parentWidget();
  269. if (row && row->property("selecting").toBool())
  270. {
  271. if (auto *cb = row->findChild<QCheckBox *>(QStringLiteral("msgCheck")))
  272. cb->toggle();
  273. return;
  274. }
  275. if (onClick)
  276. onClick();
  277. }
  278. void contextMenuEvent(QContextMenuEvent *e) override
  279. {
  280. QWidget *row = parentWidget();
  281. const bool selecting = row && row->property("selecting").toBool();
  282. if (!onCopy && !onForward && !onSaveAs && !onFavorite && !onDelete && !onMultiSelect)
  283. {
  284. QFrame::contextMenuEvent(e);
  285. return;
  286. }
  287. QMenu menu;
  288. QAction *copy = onCopy ? menu.addAction(QStringLiteral("复制")) : nullptr;
  289. QAction *fav = onFavorite ? menu.addAction(QStringLiteral("收藏")) : nullptr;
  290. QAction *fwd = onForward ? menu.addAction(QStringLiteral("转发")) : nullptr;
  291. QAction *save = onSaveAs ? menu.addAction(QStringLiteral("保存")) : nullptr;
  292. if ((copy || fav || fwd || save) && (onDelete || onMultiSelect))
  293. menu.addSeparator();
  294. QAction *del = onDelete ? menu.addAction(QStringLiteral("删除")) : nullptr;
  295. QAction *multi = nullptr;
  296. if (onMultiSelect)
  297. multi = menu.addAction(selecting ? QStringLiteral("取消多选")
  298. : QStringLiteral("多选"));
  299. QAction *picked = menu.exec(e->globalPos());
  300. if (picked && picked == copy && onCopy)
  301. onCopy();
  302. else if (picked && picked == fav && onFavorite)
  303. onFavorite();
  304. else if (picked && picked == fwd && onForward)
  305. onForward();
  306. else if (picked && picked == save && onSaveAs)
  307. onSaveAs();
  308. else if (picked && picked == del && onDelete)
  309. onDelete();
  310. else if (picked && picked == multi && onMultiSelect)
  311. onMultiSelect();
  312. }
  313. };
  314. inline QSize chatMediaBox(int srcW, int srcH, bool video)
  315. {
  316. constexpr int kMax = 180;
  317. if (srcW <= 0 || srcH <= 0)
  318. return video ? QSize(180, 101) : QSize(160, 120);
  319. QSize s(srcW, srcH);
  320. s.scale(kMax, kMax, Qt::KeepAspectRatio);
  321. return QSize(qMax(48, s.width()), qMax(48, s.height()));
  322. }
  323. inline void lockMediaBox(QWidget *w, const QSize &box)
  324. {
  325. if (!w)
  326. return;
  327. w->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  328. w->setMinimumSize(box);
  329. w->setMaximumSize(box);
  330. w->resize(box);
  331. w->updateGeometry();
  332. }
  333. inline void lockChatMediaRow(QWidget *row, QWidget *bubble, QWidget *nameCol = nullptr)
  334. {
  335. if (!row || !bubble)
  336. return;
  337. int h = bubble->minimumSize().height();
  338. if (h < 48)
  339. h = bubble->sizeHint().height();
  340. if (nameCol)
  341. {
  342. const int colH = h + 18;
  343. nameCol->setMinimumHeight(colH);
  344. nameCol->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum);
  345. h = colH;
  346. }
  347. row->setMinimumHeight(qMax(36, h));
  348. row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum);
  349. row->updateGeometry();
  350. }
  351. inline void paintChatMedia(QPainter *p, const QRect &r, const QPixmap &pm, const QColor &fill)
  352. {
  353. p->setRenderHint(QPainter::Antialiasing);
  354. p->setRenderHint(QPainter::SmoothPixmapTransform);
  355. QPainterPath path;
  356. path.addRoundedRect(QRectF(r).adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
  357. p->setClipPath(path);
  358. p->fillRect(r, fill);
  359. if (pm.isNull())
  360. return;
  361. const qreal dpr = qMax(1.0, pm.devicePixelRatio());
  362. QSize pix(qMax(1, qRound(pm.width() / dpr)), qMax(1, qRound(pm.height() / dpr)));
  363. pix.scale(r.size(), Qt::KeepAspectRatio);
  364. QRect dest(QPoint(0, 0), pix);
  365. dest.moveCenter(r.center());
  366. p->drawPixmap(dest, pm);
  367. }
  368. inline void stickChatIfNearBottom(QWidget *w)
  369. {
  370. if (!w)
  371. return;
  372. for (QWidget *p = w->parentWidget(); p; p = p->parentWidget())
  373. {
  374. if (auto *sa = qobject_cast<QScrollArea *>(p))
  375. {
  376. QScrollBar *bar = sa->verticalScrollBar();
  377. if (bar && (bar->maximum() - bar->value()) <= 48)
  378. bar->setValue(bar->maximum());
  379. return;
  380. }
  381. }
  382. }
  383. inline QPixmap fileCtlIcon(bool pause, bool upload = false)
  384. {
  385. const int size = 22;
  386. const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
  387. QPixmap pm(qRound(size * dpr), qRound(size * dpr));
  388. pm.fill(Qt::transparent);
  389. pm.setDevicePixelRatio(dpr);
  390. QPainter p(&pm);
  391. p.setRenderHint(QPainter::Antialiasing);
  392. const QColor fg(QStringLiteral("#576B95"));
  393. p.setPen(QPen(fg, 1.4));
  394. p.setBrush(Qt::NoBrush);
  395. p.drawEllipse(QRectF(1.2, 1.2, size - 2.4, size - 2.4));
  396. if (pause)
  397. {
  398. p.setPen(Qt::NoPen);
  399. p.setBrush(fg);
  400. p.drawRoundedRect(QRectF(7.2, 6.4, 2.6, 9.2), 0.8, 0.8);
  401. p.drawRoundedRect(QRectF(12.2, 6.4, 2.6, 9.2), 0.8, 0.8);
  402. }
  403. else
  404. {
  405. p.setPen(QPen(fg, 1.6, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
  406. if (upload)
  407. {
  408. p.drawLine(QPointF(11.0, 14.4), QPointF(11.0, 8.0));
  409. p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 6.4));
  410. p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 6.4));
  411. }
  412. else
  413. {
  414. p.drawLine(QPointF(11.0, 6.2), QPointF(11.0, 12.6));
  415. p.drawLine(QPointF(7.8, 10.4), QPointF(11.0, 14.4));
  416. p.drawLine(QPointF(14.2, 10.4), QPointF(11.0, 14.4));
  417. }
  418. }
  419. return pm;
  420. }
  421. inline QPushButton *fileCtlBtn(QWidget *parent, bool pause, bool upload = false)
  422. {
  423. auto *btn = new QPushButton(parent);
  424. btn->setFixedSize(22, 22);
  425. btn->setCursor(Qt::PointingHandCursor);
  426. btn->setFocusPolicy(Qt::NoFocus);
  427. btn->setFlat(true);
  428. btn->setIcon(QIcon(fileCtlIcon(pause, upload)));
  429. btn->setIconSize(QSize(22, 22));
  430. btn->setStyleSheet(QStringLiteral(
  431. "QPushButton { background:transparent; border:none; padding:0; }"
  432. "QPushButton:disabled { opacity:0.35; }"));
  433. return btn;
  434. }
  435. class ImageBubble : public ClickWidget
  436. {
  437. public:
  438. ImageBubble(QWidget *parent, qint64 fileId, const QString &name, const QString &url,
  439. const QString &mime, int hintW = 0, int hintH = 0)
  440. : ClickWidget(parent)
  441. , m_fileId(fileId)
  442. , m_url(url)
  443. , m_mime(mime)
  444. , m_name(name)
  445. {
  446. setObjectName(QStringLiteral("imageBubble"));
  447. setFrameShape(QFrame::NoFrame);
  448. setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  449. FileCache::instance().bindMeta(fileId, url, mime, name, 0);
  450. if (hintW > 0 && hintH > 0)
  451. FileCache::instance().rememberMediaSize(fileId, hintW, hintH);
  452. QSize src = FileCache::instance().probeImageSize(fileId);
  453. if (src.isEmpty())
  454. src = QSize(hintW, hintH);
  455. m_box = chatMediaBox(src.width(), src.height(), false);
  456. lockMediaBox(this, m_box);
  457. connect(&FileCache::instance(), &FileCache::thumbReady, this,
  458. [this](qint64 id, int edge) {
  459. if (id == m_fileId && edge == 180)
  460. applyThumb(FileCache::instance().cachedThumbnail(m_fileId, 180));
  461. });
  462. connect(&FileCache::instance(), &FileCache::ready, this, [this](qint64 id) {
  463. if (id != m_fileId || !m_asked)
  464. return;
  465. FileCache::instance().requestThumbnail(m_fileId, 180);
  466. });
  467. }
  468. QSize sizeHint() const override { return m_box; }
  469. QSize minimumSizeHint() const override { return m_box; }
  470. bool hasHeightForWidth() const override { return false; }
  471. void ensureMedia()
  472. {
  473. if (m_asked)
  474. return;
  475. m_asked = true;
  476. const QPixmap pm = FileCache::instance().cachedThumbnail(m_fileId, 180);
  477. if (!pm.isNull())
  478. {
  479. applyThumb(pm);
  480. return;
  481. }
  482. FileCache::instance().requestThumbnail(m_fileId, 180);
  483. FileCache::instance().ensure(m_fileId, m_url, m_mime, m_name);
  484. }
  485. protected:
  486. void paintEvent(QPaintEvent *) override
  487. {
  488. QPainter p(this);
  489. paintChatMedia(&p, rect(), m_pm, QColor(QStringLiteral("#F0F0F0")));
  490. }
  491. private:
  492. void applyThumb(const QPixmap &got)
  493. {
  494. if (got.isNull())
  495. return;
  496. m_pm = got;
  497. update();
  498. }
  499. qint64 m_fileId = 0;
  500. QString m_url;
  501. QString m_mime;
  502. QString m_name;
  503. bool m_asked = false;
  504. QSize m_box;
  505. QPixmap m_pm;
  506. };
  507. class FileBubble : public ClickWidget
  508. {
  509. public:
  510. FileBubble(QWidget *parent, qint64 fileId, const QString &name, qint64 size,
  511. const QString &url, const QString &mime, bool mine)
  512. : ClickWidget(parent)
  513. , m_fileId(fileId)
  514. , m_size(size)
  515. , m_url(url)
  516. , m_mime(mime)
  517. , m_name(name)
  518. , m_mine(mine)
  519. {
  520. setObjectName(QStringLiteral("fileBubble"));
  521. setStyleSheet(QStringLiteral("background:#FFFFFF; border:none; border-radius:4px;"));
  522. setFixedWidth(252);
  523. FileCache::instance().bindMeta(fileId, url, mime, name, size);
  524. auto *root = new QVBoxLayout(this);
  525. root->setContentsMargins(8, 8, 10, 8);
  526. root->setSpacing(6);
  527. auto *top = new QHBoxLayout();
  528. top->setContentsMargins(0, 0, 0, 0);
  529. top->setSpacing(8);
  530. auto *icon = new QLabel(this);
  531. icon->setFixedSize(40, 40);
  532. icon->setScaledContents(true);
  533. icon->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  534. if (FileCache::looksLikeImage(mime, name))
  535. {
  536. const QPixmap pm = FileCache::instance().thumbnail(fileId, 40);
  537. icon->setPixmap(pm.isNull() ? FileTypeIcon::pixmap(name, 40) : pm);
  538. }
  539. else
  540. {
  541. icon->setPixmap(FileTypeIcon::pixmap(name, 40));
  542. }
  543. auto *info = new QVBoxLayout();
  544. info->setContentsMargins(0, 0, 0, 0);
  545. info->setSpacing(2);
  546. auto *n = new QLabel(name, this);
  547. n->setWordWrap(true);
  548. n->setStyleSheet(QStringLiteral(
  549. "color:#191919; font-size:13px; background:transparent; border:none;"));
  550. m_statusLab = new QLabel(this);
  551. m_statusLab->setStyleSheet(QStringLiteral(
  552. "color:#888888; font-size:11px; background:transparent; border:none;"));
  553. info->addWidget(n);
  554. info->addWidget(m_statusLab);
  555. info->addStretch();
  556. top->addWidget(icon, 0, Qt::AlignTop);
  557. top->addLayout(info, 1);
  558. m_bar = new QProgressBar(this);
  559. m_bar->setRange(0, 1000);
  560. m_bar->setValue(0);
  561. m_bar->setTextVisible(false);
  562. m_bar->setToolTip(QString());
  563. m_bar->setFixedHeight(6);
  564. m_bar->setStyleSheet(QStringLiteral(
  565. "QProgressBar { background:rgba(0,0,0,0.08); border:none; border-radius:3px; }"
  566. "QProgressBar::chunk { background:#07C160; border-radius:3px; }"));
  567. auto *meta = new QHBoxLayout();
  568. meta->setContentsMargins(0, 0, 0, 0);
  569. meta->setSpacing(8);
  570. m_sizeLab = new QLabel(this);
  571. m_sizeLab->setStyleSheet(QStringLiteral(
  572. "color:#888888; font-size:11px; background:transparent; border:none;"));
  573. m_speedLab = new QLabel(this);
  574. m_speedLab->setStyleSheet(QStringLiteral(
  575. "color:#888888; font-size:11px; background:transparent; border:none;"));
  576. meta->addWidget(m_sizeLab, 1);
  577. meta->addWidget(m_speedLab, 0, Qt::AlignRight);
  578. root->addLayout(top);
  579. root->addWidget(m_bar);
  580. root->addLayout(meta);
  581. m_bar->hide();
  582. m_sizeLab->hide();
  583. m_speedLab->hide();
  584. connect(&FileCache::instance(), &FileCache::statusChanged, this, [this](qint64 id) {
  585. if (id == m_fileId)
  586. syncUi();
  587. });
  588. connect(&FileCache::instance(), &FileCache::progress, this,
  589. [this](qint64 id, qint64, qint64, double) {
  590. if (id == m_fileId)
  591. syncUi();
  592. });
  593. connect(&FileCache::instance(), &FileCache::failed, this,
  594. [this](qint64 id, const QString &error) {
  595. if (id != m_fileId)
  596. return;
  597. const bool uploading = FileCache::instance().needsUpload(m_fileId)
  598. || FileCache::instance().status(m_fileId) == FileCache::Uploading;
  599. if (!uploading)
  600. {
  601. UiHelpers::showToast(window(),
  602. error.isEmpty() ? QStringLiteral("下载失败") : error,
  603. true);
  604. }
  605. syncUi();
  606. });
  607. onClick = [this]() {
  608. const QString path = FileCache::instance().localPath(m_fileId);
  609. if (!path.isEmpty())
  610. {
  611. if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path)))
  612. UiHelpers::showToast(window(), QStringLiteral("无法打开文件"), true);
  613. return;
  614. }
  615. if (FileCache::instance().needsUpload(m_fileId))
  616. {
  617. FileCache::instance().startUpload(m_fileId);
  618. return;
  619. }
  620. m_wantReceive = true;
  621. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  622. syncUi();
  623. };
  624. onSaveAs = [this]() {
  625. const QString src = FileCache::instance().localPath(m_fileId);
  626. if (src.isEmpty())
  627. {
  628. UiHelpers::showToast(window(), QStringLiteral("请先下载文件"), true);
  629. return;
  630. }
  631. const QString dest = QFileDialog::getSaveFileName(window(), QStringLiteral("保存文件"), m_name);
  632. if (dest.isEmpty())
  633. return;
  634. QFile::remove(dest);
  635. if (!QFile::copy(src, dest))
  636. UiHelpers::showToast(window(), QStringLiteral("保存失败"), true);
  637. else
  638. UiHelpers::showToast(window(), QStringLiteral("已保存"));
  639. };
  640. syncUi();
  641. }
  642. void bindActionSlot(QWidget *slot, QLabel *sendMark)
  643. {
  644. m_actionSlot = slot;
  645. m_sendMark = sendMark;
  646. if (!m_actionSlot)
  647. return;
  648. m_dl = fileCtlBtn(m_actionSlot, false, false);
  649. m_pause = fileCtlBtn(m_actionSlot, true);
  650. m_dl->setParent(m_actionSlot);
  651. m_pause->setParent(m_actionSlot);
  652. m_dl->move(0, 0);
  653. m_pause->move(0, 0);
  654. m_dl->raise();
  655. m_pause->raise();
  656. connect(m_dl, &QPushButton::clicked, this, [this]() {
  657. if (FileCache::instance().needsUpload(m_fileId))
  658. FileCache::instance().startUpload(m_fileId);
  659. else
  660. {
  661. m_wantReceive = true;
  662. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  663. }
  664. });
  665. connect(m_pause, &QPushButton::clicked, this, [this]() {
  666. if (FileCache::instance().needsUpload(m_fileId)
  667. || FileCache::instance().status(m_fileId) == FileCache::Uploading)
  668. FileCache::instance().cancelUpload(m_fileId);
  669. else
  670. FileCache::instance().cancelDownload(m_fileId);
  671. });
  672. syncUi();
  673. }
  674. void refreshActionSlot()
  675. {
  676. syncUi();
  677. }
  678. private:
  679. void syncUi()
  680. {
  681. const FileCache::Status st = FileCache::instance().status(m_fileId);
  682. const bool pendingUp = m_mine && FileCache::instance().needsUpload(m_fileId);
  683. qint64 rec = 0, tot = m_size;
  684. double bps = 0;
  685. FileCache::instance().progressOf(m_fileId, &rec, &tot, &bps);
  686. if (tot <= 0)
  687. tot = m_size;
  688. const bool received = (st == FileCache::Received) && !pendingUp;
  689. const bool transferring = (st == FileCache::Downloading) || (st == FileCache::Uploading);
  690. const bool transferStarted = transferring || (rec > 0 && !received);
  691. const bool sentOnServer = m_mine && !pendingUp && m_fileId > 0;
  692. const bool showAsSent = sentOnServer && !m_wantReceive && !transferStarted;
  693. QString status = FileCache::statusText(st);
  694. if (pendingUp && st != FileCache::Uploading)
  695. status = QStringLiteral("已暂停");
  696. else if (st == FileCache::NotReceived && rec > 0)
  697. status = QStringLiteral("已暂停");
  698. if (showAsSent)
  699. status = QStringLiteral("已发送");
  700. m_statusLab->setText(status);
  701. if (showAsSent)
  702. {
  703. m_bar->hide();
  704. m_sizeLab->hide();
  705. m_speedLab->hide();
  706. }
  707. else if (transferStarted)
  708. {
  709. m_bar->show();
  710. m_sizeLab->show();
  711. m_speedLab->show();
  712. if (tot > 0)
  713. m_bar->setValue(
  714. static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
  715. else
  716. m_bar->setValue(0);
  717. m_sizeLab->setText(QStringLiteral("%1 / %2")
  718. .arg(FileCache::formatSize(rec),
  719. tot > 0 ? FileCache::formatSize(tot)
  720. : QStringLiteral("--")));
  721. if (transferring)
  722. m_speedLab->setText(FileCache::formatSpeed(bps));
  723. else
  724. m_speedLab->setText(QStringLiteral("已暂停"));
  725. }
  726. else
  727. {
  728. m_bar->hide();
  729. m_sizeLab->hide();
  730. m_speedLab->hide();
  731. }
  732. if (!m_actionSlot || !m_dl || !m_pause)
  733. return;
  734. const int sendState = parentWidget()
  735. ? parentWidget()->property("sendStatus").toInt()
  736. : LocalMsg::Ok;
  737. const bool sendBusy = m_mine
  738. && (sendState == LocalMsg::Sending || sendState == LocalMsg::Failed);
  739. if (sendBusy)
  740. {
  741. m_dl->hide();
  742. m_pause->hide();
  743. if (m_actionSlot)
  744. m_actionSlot->show();
  745. if (m_sendMark)
  746. m_sendMark->show();
  747. return;
  748. }
  749. if (received || showAsSent)
  750. {
  751. m_dl->hide();
  752. m_pause->hide();
  753. if (m_sendMark)
  754. m_sendMark->hide();
  755. if (m_actionSlot)
  756. m_actionSlot->hide();
  757. return;
  758. }
  759. if (m_actionSlot)
  760. m_actionSlot->show();
  761. if (m_sendMark)
  762. m_sendMark->hide();
  763. m_dl->setIcon(QIcon(fileCtlIcon(false, pendingUp)));
  764. m_dl->setVisible(!transferring);
  765. m_pause->setVisible(transferring);
  766. }
  767. qint64 m_fileId = 0;
  768. qint64 m_size = 0;
  769. QString m_url;
  770. QString m_mime;
  771. QString m_name;
  772. bool m_mine = false;
  773. bool m_wantReceive = false;
  774. QWidget *m_actionSlot = nullptr;
  775. QLabel *m_sendMark = nullptr;
  776. QPushButton *m_dl = nullptr;
  777. QPushButton *m_pause = nullptr;
  778. QLabel *m_statusLab = nullptr;
  779. QProgressBar *m_bar = nullptr;
  780. QLabel *m_sizeLab = nullptr;
  781. QLabel *m_speedLab = nullptr;
  782. };
  783. inline QPixmap videoPlayOverlay(int size)
  784. {
  785. const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
  786. QPixmap pm(qRound(size * dpr), qRound(size * dpr));
  787. pm.fill(Qt::transparent);
  788. pm.setDevicePixelRatio(dpr);
  789. QPainter p(&pm);
  790. p.setRenderHint(QPainter::Antialiasing, true);
  791. p.setBrush(QColor(0, 0, 0, 140));
  792. p.setPen(Qt::NoPen);
  793. p.drawEllipse(0, 0, size, size);
  794. QPainterPath tri;
  795. const qreal s = size;
  796. tri.moveTo(s * 0.38, s * 0.28);
  797. tri.lineTo(s * 0.38, s * 0.72);
  798. tri.lineTo(s * 0.74, s * 0.50);
  799. tri.closeSubpath();
  800. p.setBrush(QColor(255, 255, 255, 230));
  801. p.drawPath(tri);
  802. return pm;
  803. }
  804. class VideoBubble : public ClickWidget
  805. {
  806. public:
  807. VideoBubble(QWidget *parent, qint64 fileId, const QString &name, qint64 size,
  808. const QString &url, const QString &mime, bool mine, int hintW = 0,
  809. int hintH = 0)
  810. : ClickWidget(parent)
  811. , m_fileId(fileId)
  812. , m_size(size)
  813. , m_url(url)
  814. , m_mime(mime)
  815. , m_name(name)
  816. , m_mine(mine)
  817. , m_hintW(hintW)
  818. , m_hintH(hintH)
  819. {
  820. setObjectName(QStringLiteral("videoBubble"));
  821. setFrameShape(QFrame::NoFrame);
  822. setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  823. FileCache::instance().bindMeta(fileId, url, mime, name, size);
  824. if (hintW > 0 && hintH > 0)
  825. FileCache::instance().rememberMediaSize(fileId, hintW, hintH);
  826. m_box = reservedSize();
  827. lockMediaBox(this, m_box);
  828. m_overlay = new QWidget(this);
  829. m_overlay->setAttribute(Qt::WA_TransparentForMouseEvents);
  830. m_overlay->setStyleSheet(QStringLiteral("background:transparent;"));
  831. auto *ovLay = new QVBoxLayout(m_overlay);
  832. ovLay->setContentsMargins(6, 0, 6, 6);
  833. ovLay->setSpacing(3);
  834. ovLay->addStretch();
  835. m_metaLab = new QLabel(m_overlay);
  836. m_metaLab->setStyleSheet(QStringLiteral(
  837. "color:#FFFFFF; font-size:11px; background:transparent; border:none;"));
  838. m_bar = new QProgressBar(m_overlay);
  839. m_bar->setRange(0, 1000);
  840. m_bar->setValue(0);
  841. m_bar->setTextVisible(false);
  842. m_bar->setFixedHeight(4);
  843. m_bar->setStyleSheet(QStringLiteral(
  844. "QProgressBar { background:rgba(255,255,255,0.25); border:none; border-radius:2px; }"
  845. "QProgressBar::chunk { background:#07C160; border-radius:2px; }"));
  846. ovLay->addWidget(m_metaLab);
  847. ovLay->addWidget(m_bar);
  848. m_overlay->setGeometry(QRect(QPoint(0, 0), m_box));
  849. connect(&FileCache::instance(), &FileCache::posterReady, this, [this](qint64 id) {
  850. if (id == m_fileId)
  851. applyPoster();
  852. });
  853. connect(&FileCache::instance(), &FileCache::thumbReady, this,
  854. [this](qint64 id, int) {
  855. if (id == m_fileId)
  856. applyPoster();
  857. });
  858. connect(&FileCache::instance(), &FileCache::ready, this, [this](qint64 id) {
  859. if (id != m_fileId || !m_asked)
  860. return;
  861. FileCache::instance().requestVideoPoster(m_fileId);
  862. applyPoster();
  863. syncUi();
  864. });
  865. connect(&FileCache::instance(), &FileCache::statusChanged, this, [this](qint64 id) {
  866. if (id == m_fileId)
  867. syncUi();
  868. });
  869. connect(&FileCache::instance(), &FileCache::progress, this,
  870. [this](qint64 id, qint64, qint64, double) {
  871. if (id == m_fileId)
  872. syncUi();
  873. });
  874. connect(&FileCache::instance(), &FileCache::failed, this,
  875. [this](qint64 id, const QString &error) {
  876. if (id != m_fileId)
  877. return;
  878. const bool uploading = FileCache::instance().needsUpload(m_fileId)
  879. || FileCache::instance().status(m_fileId) == FileCache::Uploading;
  880. if (!uploading)
  881. {
  882. UiHelpers::showToast(window(),
  883. error.isEmpty() ? QStringLiteral("下载失败") : error,
  884. true);
  885. }
  886. syncUi();
  887. });
  888. onClick = [this]() {
  889. const QString path = FileCache::instance().localPath(m_fileId);
  890. if (!path.isEmpty())
  891. return;
  892. if (FileCache::instance().needsUpload(m_fileId))
  893. {
  894. FileCache::instance().startUpload(m_fileId);
  895. return;
  896. }
  897. m_wantReceive = true;
  898. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  899. syncUi();
  900. };
  901. onSaveAs = [this]() {
  902. const QString src = FileCache::instance().localPath(m_fileId);
  903. if (src.isEmpty())
  904. {
  905. UiHelpers::showToast(window(), QStringLiteral("请先下载视频"), true);
  906. return;
  907. }
  908. const QString dest = QFileDialog::getSaveFileName(window(), QStringLiteral("保存视频"),
  909. m_name);
  910. if (dest.isEmpty())
  911. return;
  912. QFile::remove(dest);
  913. if (!QFile::copy(src, dest))
  914. UiHelpers::showToast(window(), QStringLiteral("保存失败"), true);
  915. else
  916. UiHelpers::showToast(window(), QStringLiteral("已保存"));
  917. };
  918. applyPoster();
  919. syncUi();
  920. }
  921. QSize sizeHint() const override { return m_box; }
  922. QSize minimumSizeHint() const override { return m_box; }
  923. bool hasHeightForWidth() const override { return false; }
  924. void ensureMedia()
  925. {
  926. if (m_asked)
  927. return;
  928. m_asked = true;
  929. applyPoster();
  930. if (FileCache::instance().localPath(m_fileId).isEmpty() && !m_mine)
  931. {
  932. m_wantReceive = true;
  933. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  934. }
  935. else
  936. {
  937. FileCache::instance().requestVideoPoster(m_fileId);
  938. }
  939. syncUi();
  940. }
  941. void bindActionSlot(QWidget *slot, QLabel *sendMark)
  942. {
  943. m_actionSlot = slot;
  944. m_sendMark = sendMark;
  945. if (!m_actionSlot)
  946. return;
  947. m_dl = fileCtlBtn(m_actionSlot, false, false);
  948. m_pause = fileCtlBtn(m_actionSlot, true);
  949. m_dl->setParent(m_actionSlot);
  950. m_pause->setParent(m_actionSlot);
  951. m_dl->move(0, 0);
  952. m_pause->move(0, 0);
  953. m_dl->raise();
  954. m_pause->raise();
  955. connect(m_dl, &QPushButton::clicked, this, [this]() {
  956. if (FileCache::instance().needsUpload(m_fileId))
  957. FileCache::instance().startUpload(m_fileId);
  958. else
  959. {
  960. m_wantReceive = true;
  961. FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
  962. }
  963. });
  964. connect(m_pause, &QPushButton::clicked, this, [this]() {
  965. if (FileCache::instance().needsUpload(m_fileId)
  966. || FileCache::instance().status(m_fileId) == FileCache::Uploading)
  967. FileCache::instance().cancelUpload(m_fileId);
  968. else
  969. FileCache::instance().cancelDownload(m_fileId);
  970. });
  971. syncUi();
  972. }
  973. void refreshActionSlot()
  974. {
  975. syncUi();
  976. }
  977. protected:
  978. void paintEvent(QPaintEvent *) override
  979. {
  980. QPainter p(this);
  981. paintChatMedia(&p, rect(), m_pm, QColor(QStringLiteral("#1A1A1A")));
  982. const QPixmap play = videoPlayOverlay(36);
  983. const qreal dpr = qMax(1.0, play.devicePixelRatio());
  984. const QSize ps(qRound(play.width() / dpr), qRound(play.height() / dpr));
  985. QRect pr(QPoint(0, 0), ps);
  986. pr.moveCenter(rect().center());
  987. p.drawPixmap(pr, play);
  988. }
  989. void resizeEvent(QResizeEvent *e) override
  990. {
  991. ClickWidget::resizeEvent(e);
  992. if (m_overlay)
  993. m_overlay->setGeometry(rect());
  994. }
  995. private:
  996. void applyPoster()
  997. {
  998. const QPixmap got = FileCache::instance().cachedThumbnail(m_fileId, 180);
  999. if (got.isNull())
  1000. return;
  1001. m_pm = got;
  1002. update();
  1003. }
  1004. QSize reservedSize() const
  1005. {
  1006. QSize src = FileCache::instance().mediaSize(m_fileId);
  1007. if (src.isEmpty())
  1008. src = QSize(m_hintW, m_hintH);
  1009. return chatMediaBox(src.width(), src.height(), true);
  1010. }
  1011. void syncUi()
  1012. {
  1013. const FileCache::Status st = FileCache::instance().status(m_fileId);
  1014. const bool pendingUp = m_mine && FileCache::instance().needsUpload(m_fileId);
  1015. qint64 rec = 0, tot = m_size;
  1016. double bps = 0;
  1017. FileCache::instance().progressOf(m_fileId, &rec, &tot, &bps);
  1018. if (tot <= 0)
  1019. tot = m_size;
  1020. const bool received = (st == FileCache::Received) && !pendingUp;
  1021. const bool transferring = (st == FileCache::Downloading) || (st == FileCache::Uploading);
  1022. const bool transferStarted = transferring || (rec > 0 && !received);
  1023. const bool sentOnServer = m_mine && !pendingUp && m_fileId > 0;
  1024. const bool showAsSent = sentOnServer && !m_wantReceive && !transferStarted;
  1025. if (showAsSent || received || !transferStarted)
  1026. {
  1027. m_bar->hide();
  1028. m_metaLab->hide();
  1029. }
  1030. else
  1031. {
  1032. m_bar->show();
  1033. m_metaLab->show();
  1034. if (tot > 0)
  1035. m_bar->setValue(
  1036. static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
  1037. else
  1038. m_bar->setValue(0);
  1039. QString meta = QStringLiteral("%1 / %2")
  1040. .arg(FileCache::formatSize(rec),
  1041. tot > 0 ? FileCache::formatSize(tot)
  1042. : QStringLiteral("--"));
  1043. if (transferring)
  1044. meta += QStringLiteral(" ") + FileCache::formatSpeed(bps);
  1045. else
  1046. meta += QStringLiteral(" 已暂停");
  1047. m_metaLab->setText(meta);
  1048. }
  1049. if (!m_actionSlot || !m_dl || !m_pause)
  1050. return;
  1051. const int sendState = parentWidget()
  1052. ? parentWidget()->property("sendStatus").toInt()
  1053. : LocalMsg::Ok;
  1054. const bool sendBusy = m_mine
  1055. && (sendState == LocalMsg::Sending || sendState == LocalMsg::Failed);
  1056. if (sendBusy)
  1057. {
  1058. m_dl->hide();
  1059. m_pause->hide();
  1060. if (m_actionSlot)
  1061. m_actionSlot->show();
  1062. if (m_sendMark)
  1063. m_sendMark->show();
  1064. return;
  1065. }
  1066. if (received || showAsSent)
  1067. {
  1068. m_dl->hide();
  1069. m_pause->hide();
  1070. if (m_sendMark)
  1071. m_sendMark->hide();
  1072. if (m_actionSlot)
  1073. m_actionSlot->hide();
  1074. return;
  1075. }
  1076. if (m_actionSlot)
  1077. m_actionSlot->show();
  1078. if (m_sendMark)
  1079. m_sendMark->hide();
  1080. m_dl->setIcon(QIcon(fileCtlIcon(false, pendingUp)));
  1081. m_dl->setVisible(!transferring);
  1082. m_pause->setVisible(transferring);
  1083. }
  1084. qint64 m_fileId = 0;
  1085. qint64 m_size = 0;
  1086. QString m_url;
  1087. QString m_mime;
  1088. QString m_name;
  1089. bool m_mine = false;
  1090. bool m_asked = false;
  1091. bool m_wantReceive = false;
  1092. int m_hintW = 0;
  1093. int m_hintH = 0;
  1094. QSize m_box;
  1095. QPixmap m_pm;
  1096. QWidget *m_actionSlot = nullptr;
  1097. QLabel *m_sendMark = nullptr;
  1098. QPushButton *m_dl = nullptr;
  1099. QPushButton *m_pause = nullptr;
  1100. QWidget *m_overlay = nullptr;
  1101. QProgressBar *m_bar = nullptr;
  1102. QLabel *m_metaLab = nullptr;
  1103. };
  1104. inline void refreshRowFileAction(QWidget *row)
  1105. {
  1106. if (!row)
  1107. return;
  1108. if (auto *fb = row->findChild<FileBubble *>(QStringLiteral("fileBubble")))
  1109. fb->refreshActionSlot();
  1110. if (auto *vb = row->findChild<VideoBubble *>(QStringLiteral("videoBubble")))
  1111. vb->refreshActionSlot();
  1112. }
  1113. using ElideLabel = UiHelpers::ElideLabel;
  1114. class TrayPreviewRow : public QWidget
  1115. {
  1116. public:
  1117. std::function<void()> onActivate;
  1118. explicit TrayPreviewRow(QWidget *parent = nullptr)
  1119. : QWidget(parent)
  1120. {
  1121. setAttribute(Qt::WA_Hover);
  1122. setCursor(Qt::PointingHandCursor);
  1123. setAutoFillBackground(true);
  1124. setMinimumHeight(48);
  1125. applyHover(false);
  1126. }
  1127. protected:
  1128. void enterEvent(QEvent *event) override
  1129. {
  1130. applyHover(true);
  1131. QWidget::enterEvent(event);
  1132. }
  1133. void leaveEvent(QEvent *event) override
  1134. {
  1135. applyHover(false);
  1136. QWidget::leaveEvent(event);
  1137. }
  1138. void mouseReleaseEvent(QMouseEvent *event) override
  1139. {
  1140. if (event->button() == Qt::LeftButton && rect().contains(event->pos()) && onActivate)
  1141. onActivate();
  1142. QWidget::mouseReleaseEvent(event);
  1143. }
  1144. private:
  1145. void applyHover(bool on)
  1146. {
  1147. QPalette pal = palette();
  1148. pal.setColor(QPalette::Window, on ? QColor("#F2F2F2") : QColor("#FFFFFF"));
  1149. setPalette(pal);
  1150. }
  1151. };
  1152. inline QPixmap navIcon(int kind, const QColor &color)
  1153. {
  1154. const int size = 22;
  1155. QPixmap pm(size, size);
  1156. pm.fill(Qt::transparent);
  1157. QPainter p(&pm);
  1158. p.setRenderHint(QPainter::Antialiasing, true);
  1159. p.setPen(QPen(color, 1.6));
  1160. p.setBrush(Qt::NoBrush);
  1161. if (kind == 0)
  1162. {
  1163. p.drawRoundedRect(QRectF(2.5, 3.5, 17, 12.5), 2.5, 2.5);
  1164. QPainterPath tail;
  1165. tail.moveTo(6.5, 16);
  1166. tail.lineTo(8.5, 20);
  1167. tail.lineTo(12, 16);
  1168. p.setBrush(color);
  1169. p.setPen(Qt::NoPen);
  1170. p.drawPath(tail);
  1171. }
  1172. else if (kind == 1)
  1173. {
  1174. p.drawEllipse(QRectF(6.5, 3.2, 9, 9));
  1175. p.drawArc(QRect(3, 12, 16, 12), 20 * 16, 140 * 16);
  1176. }
  1177. else if (kind == 3)
  1178. {
  1179. QPolygonF star;
  1180. for (int i = 0; i < 5; ++i)
  1181. {
  1182. const qreal a = -1.5707963 + i * 1.25663706;
  1183. star << QPointF(11 + qCos(a) * 8.2, 11 + qSin(a) * 8.2);
  1184. const qreal b = a + 0.62831853;
  1185. star << QPointF(11 + qCos(b) * 3.4, 11 + qSin(b) * 3.4);
  1186. }
  1187. p.setBrush(color);
  1188. p.setPen(Qt::NoPen);
  1189. p.drawPolygon(star);
  1190. }
  1191. else
  1192. {
  1193. const QPointF c(11, 11);
  1194. for (int i = 0; i < 6; ++i)
  1195. {
  1196. const qreal a = i * 3.14159265 / 3.0;
  1197. const QPointF d(qCos(a), qSin(a));
  1198. p.drawLine(c + d * 5.2, c + d * 8.6);
  1199. }
  1200. p.drawEllipse(QRectF(6.6, 6.6, 8.8, 8.8));
  1201. p.drawEllipse(QRectF(8.6, 8.6, 4.8, 4.8));
  1202. }
  1203. return pm;
  1204. }
  1205. inline QPixmap chatToolIcon(int kind, const QColor &color, int size = 20)
  1206. {
  1207. const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
  1208. QPixmap pm(qRound(size * dpr), qRound(size * dpr));
  1209. pm.fill(Qt::transparent);
  1210. pm.setDevicePixelRatio(dpr);
  1211. QPainter p(&pm);
  1212. p.setRenderHint(QPainter::Antialiasing, true);
  1213. p.setPen(QPen(color, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
  1214. p.setBrush(Qt::NoBrush);
  1215. const qreal s = size;
  1216. if (kind == 0)
  1217. {
  1218. p.drawEllipse(QRectF(2.2, 3.2, s - 4.4, s - 5.2));
  1219. p.drawArc(QRectF(5.5, 7.5, s - 11.0, s - 12.0), 200 * 16, 140 * 16);
  1220. p.setBrush(color);
  1221. p.setPen(Qt::NoPen);
  1222. p.drawEllipse(QRectF(6.4, 7.2, 2.2, 2.2));
  1223. p.drawEllipse(QRectF(s - 8.6, 7.2, 2.2, 2.2));
  1224. }
  1225. else if (kind == 1)
  1226. {
  1227. p.drawRoundedRect(QRectF(2.5, 4.2, s - 5.0, s - 8.0), 2.0, 2.0);
  1228. p.setBrush(color);
  1229. p.setPen(Qt::NoPen);
  1230. p.drawEllipse(QRectF(5.2, 6.4, 3.0, 3.0));
  1231. QPolygonF hill;
  1232. hill << QPointF(3.6, s - 5.2) << QPointF(8.2, 10.2) << QPointF(11.2, 13.0)
  1233. << QPointF(14.4, 9.4) << QPointF(s - 3.6, s - 5.2);
  1234. p.drawPolygon(hill);
  1235. }
  1236. else if (kind == 3)
  1237. {
  1238. p.drawRoundedRect(QRectF(2.6, 4.6, s - 5.2, s - 9.0), 2.0, 2.0);
  1239. p.setBrush(color);
  1240. p.setPen(Qt::NoPen);
  1241. QPolygonF tri;
  1242. tri << QPointF(s * 0.40, s * 0.36) << QPointF(s * 0.40, s * 0.68)
  1243. << QPointF(s * 0.68, s * 0.52);
  1244. p.drawPolygon(tri);
  1245. }
  1246. else
  1247. {
  1248. p.drawRoundedRect(QRectF(5.0, 2.8, s - 9.2, s - 5.4), 1.2, 1.2);
  1249. p.drawLine(QPointF(5.0, 6.4), QPointF(s - 4.2, 6.4));
  1250. p.drawLine(QPointF(7.4, 9.4), QPointF(s - 7.2, 9.4));
  1251. p.drawLine(QPointF(7.4, 12.2), QPointF(s - 7.2, 12.2));
  1252. }
  1253. return pm;
  1254. }
  1255. inline QPixmap settingsNavIcon()
  1256. {
  1257. static QPixmap cached;
  1258. if (!cached.isNull())
  1259. return cached;
  1260. QPixmap src(QStringLiteral(":/icons/settings.png"));
  1261. if (src.isNull())
  1262. {
  1263. cached = navIcon(2, QColor("#A6A6A6"));
  1264. return cached;
  1265. }
  1266. QImage img = src.toImage().convertToFormat(QImage::Format_ARGB32);
  1267. for (int y = 0; y < img.height(); ++y)
  1268. {
  1269. auto *line = reinterpret_cast<QRgb *>(img.scanLine(y));
  1270. for (int x = 0; x < img.width(); ++x)
  1271. {
  1272. if (qRed(line[x]) < 40 && qGreen(line[x]) < 40 && qBlue(line[x]) < 40)
  1273. line[x] = qRgba(0, 0, 0, 0);
  1274. }
  1275. }
  1276. cached = QPixmap::fromImage(img).scaled(22, 22, Qt::KeepAspectRatio, Qt::SmoothTransformation);
  1277. return cached;
  1278. }
  1279. inline QPushButton *navBtn(int kind, const QString &tip, QWidget *parent, bool checkable = true)
  1280. {
  1281. auto *btn = new QPushButton(parent);
  1282. btn->setCheckable(checkable);
  1283. btn->setFixedSize(56, 48);
  1284. btn->setCursor(Qt::PointingHandCursor);
  1285. btn->setIconSize(QSize(22, 22));
  1286. btn->setIcon(QIcon(kind == 2 ? settingsNavIcon() : navIcon(kind, QColor("#A6A6A6"))));
  1287. btn->setStyleSheet(QStringLiteral(
  1288. "QPushButton { background:transparent; border:none; border-radius:0; padding:0; }"
  1289. "QPushButton:hover { background:#3A3A3A; }"
  1290. "QPushButton:checked { background:transparent; }"));
  1291. if (checkable)
  1292. {
  1293. QObject::connect(btn, &QPushButton::toggled, btn, [btn, kind](bool on) {
  1294. btn->setIcon(QIcon(navIcon(kind, on ? QColor("#07C160") : QColor("#A6A6A6"))));
  1295. });
  1296. }
  1297. return btn;
  1298. }
  1299. inline QLabel *makeOnlineDot(QWidget *parent, bool online)
  1300. {
  1301. auto *dot = new QLabel(parent);
  1302. dot->setFixedSize(8, 8);
  1303. dot->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  1304. dot->setAttribute(Qt::WA_StyledBackground, true);
  1305. dot->setStyleSheet(online
  1306. ? QStringLiteral("background:#07C160; border:none; border-radius:4px;")
  1307. : QStringLiteral("background:#C8C8C8; border:none; border-radius:4px;"));
  1308. return dot;
  1309. }
  1310. inline Conv convFromItem(const QListWidgetItem *item)
  1311. {
  1312. Conv c;
  1313. c.uid = item->data(RolePeerUid).toLongLong();
  1314. c.convType = item->data(RoleConvType).toInt();
  1315. c.name = item->data(RoleName).toString();
  1316. c.preview = item->data(RolePreview).toString();
  1317. c.time = formatTs(item->data(RoleTs).toLongLong());
  1318. c.unread = item->data(RoleUnread).toInt();
  1319. c.avatarVer = item->data(RoleAvatarVer).toInt();
  1320. c.online = item->data(RoleOnline).toInt() == 1;
  1321. c.memberCount = item->data(RoleMemberCount).toInt();
  1322. return c;
  1323. }
  1324. inline bool listOnlineOf(const QListWidget *list, qint64 uid)
  1325. {
  1326. if (!list)
  1327. return false;
  1328. for (int i = 0; i < list->count(); ++i)
  1329. {
  1330. auto *item = list->item(i);
  1331. if (item && item->data(RolePeerUid).toLongLong() == uid)
  1332. return item->data(RoleOnline).toInt() == 1;
  1333. }
  1334. return false;
  1335. }
  1336. inline QIcon blankTrayIcon()
  1337. {
  1338. QPixmap pm(32, 32);
  1339. pm.fill(Qt::transparent);
  1340. return QIcon(pm);
  1341. }
  1342. inline QWidget *makeFriendRow(const QString &name, bool online, qint64 uid, int avatarVer, QWidget *parent)
  1343. {
  1344. auto *row = new QWidget(parent);
  1345. row->setFixedHeight(48);
  1346. row->setMinimumWidth(0);
  1347. row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
  1348. row->setAttribute(Qt::WA_TranslucentBackground);
  1349. row->setStyleSheet(QStringLiteral("background:transparent;"));
  1350. auto *lay = new QHBoxLayout(row);
  1351. lay->setContentsMargins(12, 8, 12, 8);
  1352. lay->setSpacing(10);
  1353. auto *avatar = new QLabel(row);
  1354. avatar->setFixedSize(32, 32);
  1355. avatar->setPixmap(AvatarCache::instance().pixmap(uid, avatarVer, name, 32));
  1356. auto *nameLab = new ElideLabel(name, row);
  1357. nameLab->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
  1358. lay->addWidget(avatar, 0, Qt::AlignVCenter);
  1359. lay->addWidget(nameLab, 1, Qt::AlignVCenter);
  1360. if (online)
  1361. lay->addWidget(makeOnlineDot(row, true), 0, Qt::AlignVCenter);
  1362. return row;
  1363. }
  1364. inline QWidget *makeConvRow(const Conv &c, QWidget *parent)
  1365. {
  1366. auto *row = new QWidget(parent);
  1367. row->setFixedHeight(68);
  1368. row->setMinimumWidth(0);
  1369. row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
  1370. row->setAttribute(Qt::WA_TranslucentBackground);
  1371. row->setStyleSheet(QStringLiteral("background:transparent;"));
  1372. auto *grid = new QGridLayout(row);
  1373. grid->setContentsMargins(12, 10, 12, 10);
  1374. grid->setHorizontalSpacing(10);
  1375. grid->setVerticalSpacing(3);
  1376. grid->setColumnStretch(1, 1);
  1377. grid->setColumnMinimumWidth(1, 0);
  1378. auto *avatarBox = new QWidget(row);
  1379. avatarBox->setFixedSize(44, 44);
  1380. auto *avatar = new QLabel(avatarBox);
  1381. avatar->setGeometry(0, 0, 40, 40);
  1382. avatar->setPixmap(c.convType == ConvGroup
  1383. ? groupIcon(40)
  1384. : AvatarCache::instance().pixmap(c.uid, c.avatarVer, c.name, 40));
  1385. avatar->setScaledContents(false);
  1386. if (c.convType == ConvGroup)
  1387. AvatarCache::instance().applyToGroup(avatar, c.uid, c.avatarVer, c.name, 40);
  1388. else
  1389. AvatarCache::instance().applyTo(avatar, c.uid, c.avatarVer, c.name, 40);
  1390. if (c.online)
  1391. {
  1392. auto *dot = makeOnlineDot(avatarBox, true);
  1393. dot->setFixedSize(10, 10);
  1394. dot->move(30, 30);
  1395. dot->setStyleSheet(QStringLiteral(
  1396. "background:#07C160; border:2px solid #FFFFFF; border-radius:5px;"));
  1397. dot->raise();
  1398. }
  1399. if (c.unread > 0)
  1400. {
  1401. auto *badge = new QLabel(c.unread > 99 ? QStringLiteral("99") : QString::number(c.unread), avatarBox);
  1402. badge->setObjectName(QStringLiteral("unreadBadge"));
  1403. badge->setAlignment(Qt::AlignCenter);
  1404. badge->setFixedSize(16, 16);
  1405. badge->move(28, 0);
  1406. badge->setStyleSheet(QStringLiteral(
  1407. "background:#FA5151; color:#FFFFFF; font-size:10px; border:none; border-radius:8px;"));
  1408. badge->raise();
  1409. }
  1410. auto *nameRow = new QWidget(row);
  1411. nameRow->setMinimumWidth(0);
  1412. nameRow->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
  1413. nameRow->setStyleSheet(QStringLiteral("background:transparent;"));
  1414. auto *nameLay = new QHBoxLayout(nameRow);
  1415. nameLay->setContentsMargins(0, 0, 0, 0);
  1416. nameLay->setSpacing(6);
  1417. auto *name = new ElideLabel(
  1418. c.convType == ConvGroup ? groupTitle(c.name, c.memberCount) : c.name, nameRow);
  1419. name->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
  1420. nameLay->addWidget(name, 1, Qt::AlignVCenter);
  1421. auto *prevWrap = new QWidget(row);
  1422. prevWrap->setMinimumWidth(0);
  1423. prevWrap->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
  1424. prevWrap->setStyleSheet(QStringLiteral("background:transparent;"));
  1425. auto *prevLay = new QHBoxLayout(prevWrap);
  1426. prevLay->setContentsMargins(0, 0, 0, 0);
  1427. prevLay->setSpacing(4);
  1428. QString previewText = c.preview;
  1429. int iconKind = -1;
  1430. if (previewText.startsWith(QStringLiteral("[表情]")))
  1431. {
  1432. iconKind = 0;
  1433. previewText = previewText.mid(4);
  1434. if (previewText.isEmpty())
  1435. previewText = QStringLiteral("表情");
  1436. }
  1437. else if (previewText.startsWith(QStringLiteral("[图片]")))
  1438. {
  1439. iconKind = 1;
  1440. previewText = previewText.mid(4);
  1441. if (previewText.isEmpty())
  1442. previewText = QStringLiteral("图片");
  1443. }
  1444. else if (previewText.startsWith(QStringLiteral("[视频]")))
  1445. {
  1446. iconKind = 3;
  1447. previewText = previewText.mid(4);
  1448. if (previewText.isEmpty())
  1449. previewText = QStringLiteral("视频");
  1450. }
  1451. else if (previewText.startsWith(QStringLiteral("[文件]")))
  1452. {
  1453. iconKind = 2;
  1454. previewText = previewText.mid(4).trimmed();
  1455. if (previewText.isEmpty())
  1456. previewText = QStringLiteral("文件");
  1457. }
  1458. if (iconKind >= 0)
  1459. {
  1460. auto *ic = new QLabel(prevWrap);
  1461. ic->setFixedSize(14, 14);
  1462. ic->setPixmap(chatToolIcon(iconKind, QColor("#9A9A9A"), 14));
  1463. ic->setStyleSheet(QStringLiteral("background:transparent; border:none;"));
  1464. prevLay->addWidget(ic, 0, Qt::AlignVCenter);
  1465. }
  1466. auto *preview = new ElideLabel(previewText, prevWrap);
  1467. preview->setStyleSheet(QStringLiteral("color:#9A9A9A; font-size:12px; background:transparent;"));
  1468. prevLay->addWidget(preview, 1);
  1469. auto *time = new QLabel(c.time, row);
  1470. time->setStyleSheet(QStringLiteral("color:#B2B2B2; font-size:11px; background:transparent;"));
  1471. time->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
  1472. time->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
  1473. time->setMinimumWidth(time->fontMetrics().horizontalAdvance(
  1474. c.time.isEmpty() ? QStringLiteral("00:00") : c.time));
  1475. grid->addWidget(avatarBox, 0, 0, 2, 1, Qt::AlignVCenter);
  1476. grid->addWidget(nameRow, 0, 1);
  1477. grid->addWidget(time, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
  1478. grid->addWidget(prevWrap, 1, 1, 1, 2);
  1479. return row;
  1480. }
  1481. } // namespace ClientInternal