|
@@ -0,0 +1,996 @@
|
|
|
|
|
+#include "FavoritePanel.h"
|
|
|
|
|
+#include "AppConfig.h"
|
|
|
|
|
+#include "AvatarCache.h"
|
|
|
|
|
+#include "ConfirmDialog.h"
|
|
|
|
|
+#include "FileCache.h"
|
|
|
|
|
+#include "FileTypeIcon.h"
|
|
|
|
|
+#include "ImClient.h"
|
|
|
|
|
+#include "Protocol.h"
|
|
|
|
|
+#include "UiHelpers.h"
|
|
|
|
|
+
|
|
|
|
|
+#include <QApplication>
|
|
|
|
|
+#include <QDate>
|
|
|
|
|
+#include <QDateTime>
|
|
|
|
|
+#include <QDesktopServices>
|
|
|
|
|
+#include <QEvent>
|
|
|
|
|
+#include <QFileDialog>
|
|
|
|
|
+#include <QFileInfo>
|
|
|
|
|
+#include <QColor>
|
|
|
|
|
+#include <QFont>
|
|
|
|
|
+#include <QIcon>
|
|
|
|
|
+#include <QFrame>
|
|
|
|
|
+#include <QHBoxLayout>
|
|
|
|
|
+#include <QJsonArray>
|
|
|
|
|
+#include <QJsonDocument>
|
|
|
|
|
+#include <QKeyEvent>
|
|
|
|
|
+#include <QKeySequence>
|
|
|
|
|
+#include <QShortcut>
|
|
|
|
|
+#include <QLabel>
|
|
|
|
|
+#include <QAbstractItemView>
|
|
|
|
|
+#include <QListView>
|
|
|
|
|
+#include <QListWidget>
|
|
|
|
|
+#include <QProgressBar>
|
|
|
|
|
+#include <QMenu>
|
|
|
|
|
+#include <QMimeDatabase>
|
|
|
|
|
+#include <QMouseEvent>
|
|
|
|
|
+#include <QPainter>
|
|
|
|
|
+#include <QPen>
|
|
|
|
|
+#include <QPixmap>
|
|
|
|
|
+#include <QPolygonF>
|
|
|
|
|
+#include <QPointer>
|
|
|
|
|
+#include <QPushButton>
|
|
|
|
|
+#include <QRegularExpression>
|
|
|
|
|
+#include <QSizePolicy>
|
|
|
|
|
+#include <QStackedWidget>
|
|
|
|
|
+#include <QTextCharFormat>
|
|
|
|
|
+#include <QTextCursor>
|
|
|
|
|
+#include <QTextDocument>
|
|
|
|
|
+#include <QTextEdit>
|
|
|
|
|
+#include <QTextImageFormat>
|
|
|
|
|
+#include <QTextList>
|
|
|
|
|
+#include <QUrl>
|
|
|
|
|
+#include <QVBoxLayout>
|
|
|
|
|
+
|
|
|
|
|
+namespace
|
|
|
|
|
+{
|
|
|
|
|
+ enum {
|
|
|
|
|
+ RoleId = Qt::UserRole,
|
|
|
|
|
+ RoleKind = Qt::UserRole + 1,
|
|
|
|
|
+ RoleTitle = Qt::UserRole + 2
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ QString htmlToPlain(const QString &raw)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (raw.isEmpty())
|
|
|
|
|
+ return QString();
|
|
|
|
|
+ QString s = raw;
|
|
|
|
|
+ if (s.contains(QLatin1Char('<')) || s.contains(QLatin1Char('&')))
|
|
|
|
|
+ {
|
|
|
|
|
+ QTextDocument doc;
|
|
|
|
|
+ doc.setHtml(s);
|
|
|
|
|
+ s = doc.toPlainText();
|
|
|
|
|
+ }
|
|
|
|
|
+ s.remove(QRegularExpression(QStringLiteral(R"([^{}<\n]{0,48}\{[^}]{0,240}\})")));
|
|
|
|
|
+ return s.simplified();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QPixmap toolIcon(int kind, const QColor &color, int size = 16)
|
|
|
|
|
+ {
|
|
|
|
|
+ const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
|
|
|
+ QPixmap pm(qRound(size * dpr), qRound(size * dpr));
|
|
|
|
|
+ pm.fill(Qt::transparent);
|
|
|
|
|
+ pm.setDevicePixelRatio(dpr);
|
|
|
|
|
+ QPainter p(&pm);
|
|
|
|
|
+ p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
|
+ p.setPen(QPen(color, 1.4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
|
|
|
|
+ p.setBrush(Qt::NoBrush);
|
|
|
|
|
+ const qreal s = size;
|
|
|
|
|
+ if (kind == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ p.drawLine(QPointF(3.0, 4.2), QPointF(5.4, 4.2));
|
|
|
|
|
+ p.drawLine(QPointF(7.2, 4.2), QPointF(s - 2.4, 4.2));
|
|
|
|
|
+ p.drawLine(QPointF(3.0, 8.0), QPointF(5.4, 8.0));
|
|
|
|
|
+ p.drawLine(QPointF(7.2, 8.0), QPointF(s - 2.4, 8.0));
|
|
|
|
|
+ p.drawLine(QPointF(3.0, 11.8), QPointF(5.4, 11.8));
|
|
|
|
|
+ p.drawLine(QPointF(7.2, 11.8), QPointF(s - 2.4, 11.8));
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (kind == 1)
|
|
|
|
|
+ {
|
|
|
|
|
+ p.drawRoundedRect(QRectF(2.0, 3.4, s - 4.0, s - 6.4), 1.6, 1.6);
|
|
|
|
|
+ p.setBrush(color);
|
|
|
|
|
+ p.setPen(Qt::NoPen);
|
|
|
|
|
+ p.drawEllipse(QRectF(4.2, 5.2, 2.4, 2.4));
|
|
|
|
|
+ QPolygonF hill;
|
|
|
|
|
+ hill << QPointF(2.8, s - 4.2) << QPointF(6.4, 8.4) << QPointF(9.0, 10.6)
|
|
|
|
|
+ << QPointF(11.6, 7.6) << QPointF(s - 2.8, s - 4.2);
|
|
|
|
|
+ p.drawPolygon(hill);
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (kind == 2)
|
|
|
|
|
+ {
|
|
|
|
|
+ p.drawRoundedRect(QRectF(4.0, 2.2, s - 7.2, s - 4.4), 1.1, 1.1);
|
|
|
|
|
+ p.drawLine(QPointF(4.0, 5.4), QPointF(s - 3.2, 5.4));
|
|
|
|
|
+ p.drawLine(QPointF(6.0, 8.0), QPointF(s - 5.6, 8.0));
|
|
|
|
|
+ p.drawLine(QPointF(6.0, 10.4), QPointF(s - 5.6, 10.4));
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (kind == 3)
|
|
|
|
|
+ {
|
|
|
|
|
+ p.drawEllipse(QRectF(5.0, 2.4, s - 10.0, s - 10.0));
|
|
|
|
|
+ p.drawArc(QRectF(3.2, 8.6, s - 6.4, s - 7.0), 20 * 16, 140 * 16);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ p.drawRoundedRect(QRectF(3.2, 2.2, s - 6.4, s - 4.2), 1.1, 1.1);
|
|
|
|
|
+ p.drawLine(QPointF(5.2, 5.6), QPointF(s - 5.2, 5.6));
|
|
|
|
|
+ p.drawLine(QPointF(5.2, 8.0), QPointF(s - 5.2, 8.0));
|
|
|
|
|
+ p.drawLine(QPointF(5.2, 10.4), QPointF(s - 6.4, 10.4));
|
|
|
|
|
+ }
|
|
|
|
|
+ return pm;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QPixmap kindIcon(int kind)
|
|
|
|
|
+ {
|
|
|
|
|
+ const QColor color(QStringLiteral("#07C160"));
|
|
|
|
|
+ if (kind == im::FavKind::Image)
|
|
|
|
|
+ return toolIcon(1, color, 16);
|
|
|
|
|
+ if (kind == im::FavKind::File)
|
|
|
|
|
+ return toolIcon(2, color, 16);
|
|
|
|
|
+ if (kind == im::FavKind::Card)
|
|
|
|
|
+ return toolIcon(3, color, 16);
|
|
|
|
|
+ return toolIcon(4, color, 16);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QString kindLabel(int kind)
|
|
|
|
|
+ {
|
|
|
|
|
+ switch (kind)
|
|
|
|
|
+ {
|
|
|
|
|
+ case im::FavKind::Image: return QStringLiteral("图片");
|
|
|
|
|
+ case im::FavKind::File: return QStringLiteral("文件");
|
|
|
|
|
+ case im::FavKind::Card: return QStringLiteral("名片");
|
|
|
|
|
+ case im::FavKind::Note: return QStringLiteral("笔记");
|
|
|
|
|
+ default: return QStringLiteral("文本");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QString formatTs(qint64 ms)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (ms <= 0)
|
|
|
|
|
+ return QString();
|
|
|
|
|
+ const QDateTime dt = QDateTime::fromMSecsSinceEpoch(ms);
|
|
|
|
|
+ if (dt.date() == QDate::currentDate())
|
|
|
|
|
+ return dt.toString(QStringLiteral("HH:mm"));
|
|
|
|
|
+ return dt.toString(QStringLiteral("MM-dd"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QWidget *makeRow(int kind, const QString &title, qint64 ts, QWidget *parent)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *row = new QWidget(parent);
|
|
|
|
|
+ row->setMinimumWidth(0);
|
|
|
|
|
+ row->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
|
|
|
|
|
+ row->setAttribute(Qt::WA_TranslucentBackground);
|
|
|
|
|
+ auto *lay = new QHBoxLayout(row);
|
|
|
|
|
+ lay->setContentsMargins(12, 8, 12, 8);
|
|
|
|
|
+ lay->setSpacing(8);
|
|
|
|
|
+ auto *tag = new QLabel(row);
|
|
|
|
|
+ tag->setFixedSize(18, 18);
|
|
|
|
|
+ tag->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
|
+ tag->setPixmap(kindIcon(kind));
|
|
|
|
|
+ tag->setStyleSheet(QStringLiteral("background:transparent;"));
|
|
|
|
|
+ auto *name = new UiHelpers::ElideLabel(title.isEmpty() ? QStringLiteral("(无标题)") : title, row);
|
|
|
|
|
+ name->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#191919; font-size:13px; background:transparent;"));
|
|
|
|
|
+ const QString tsText = formatTs(ts);
|
|
|
|
|
+ auto *time = new QLabel(tsText, row);
|
|
|
|
|
+ time->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#B0B0B0; font-size:11px; background:transparent;"));
|
|
|
|
|
+ time->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
|
|
|
|
+ time->setMinimumWidth(time->fontMetrics().horizontalAdvance(
|
|
|
|
|
+ tsText.isEmpty() ? QStringLiteral("00:00") : tsText));
|
|
|
|
|
+ lay->addWidget(tag, 0, Qt::AlignVCenter);
|
|
|
|
|
+ lay->addWidget(name, 1, Qt::AlignVCenter);
|
|
|
|
|
+ lay->addWidget(time, 0, Qt::AlignVCenter);
|
|
|
|
|
+ return row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ QVector<qint64> favFileIds(const QString &html)
|
|
|
|
|
+ {
|
|
|
|
|
+ QVector<qint64> ids;
|
|
|
|
|
+ QRegularExpression re(QStringLiteral("favfile:(\\d+)"));
|
|
|
|
|
+ auto it = re.globalMatch(html);
|
|
|
|
|
+ while (it.hasNext())
|
|
|
|
|
+ {
|
|
|
|
|
+ const qint64 id = it.next().captured(1).toLongLong();
|
|
|
|
|
+ if (id > 0 && !ids.contains(id))
|
|
|
|
|
+ ids.push_back(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ return ids;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ class FavFileCard : public QWidget
|
|
|
|
|
+ {
|
|
|
|
|
+ public:
|
|
|
|
|
+ FavFileCard(QWidget *parent, qint64 fileId, const QString &name, qint64 size,
|
|
|
|
|
+ const QString &url, const QString &mime)
|
|
|
|
|
+ : QWidget(parent)
|
|
|
|
|
+ , m_fileId(fileId)
|
|
|
|
|
+ , m_size(size)
|
|
|
|
|
+ , m_url(url)
|
|
|
|
|
+ , m_mime(mime)
|
|
|
|
|
+ , m_name(name)
|
|
|
|
|
+ {
|
|
|
|
|
+ setStyleSheet(QStringLiteral("background:#FFFFFF; border-radius:4px;"));
|
|
|
|
|
+ setFixedWidth(320);
|
|
|
|
|
+ FileCache::instance().bindMeta(fileId, url, mime, name, size);
|
|
|
|
|
+
|
|
|
|
|
+ auto *root = new QVBoxLayout(this);
|
|
|
|
|
+ root->setContentsMargins(12, 10, 12, 10);
|
|
|
|
|
+ root->setSpacing(6);
|
|
|
|
|
+
|
|
|
|
|
+ auto *top = new QHBoxLayout();
|
|
|
|
|
+ top->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ top->setSpacing(8);
|
|
|
|
|
+
|
|
|
|
|
+ auto *icon = new QLabel(this);
|
|
|
|
|
+ icon->setFixedSize(40, 40);
|
|
|
|
|
+ icon->setPixmap(FileTypeIcon::pixmap(name, 40));
|
|
|
|
|
+
|
|
|
|
|
+ auto *info = new QVBoxLayout();
|
|
|
|
|
+ info->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ info->setSpacing(2);
|
|
|
|
|
+ auto *n = new UiHelpers::ElideLabel(name, this);
|
|
|
|
|
+ n->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#191919; font-size:13px; background:transparent;"));
|
|
|
|
|
+ m_statusLab = new QLabel(this);
|
|
|
|
|
+ m_statusLab->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#888888; font-size:11px; background:transparent;"));
|
|
|
|
|
+ info->addWidget(n);
|
|
|
|
|
+ info->addWidget(m_statusLab);
|
|
|
|
|
+
|
|
|
|
|
+ m_action = UiHelpers::ghostButton(QStringLiteral("下载"), this);
|
|
|
|
|
+ m_action->setFixedSize(64, 26);
|
|
|
|
|
+ m_action->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
|
+
|
|
|
|
|
+ top->addWidget(icon, 0, Qt::AlignTop);
|
|
|
|
|
+ top->addLayout(info, 1);
|
|
|
|
|
+ top->addWidget(m_action, 0, Qt::AlignVCenter);
|
|
|
|
|
+
|
|
|
|
|
+ m_bar = new QProgressBar(this);
|
|
|
|
|
+ m_bar->setRange(0, 1000);
|
|
|
|
|
+ m_bar->setValue(0);
|
|
|
|
|
+ m_bar->setTextVisible(false);
|
|
|
|
|
+ m_bar->setToolTip(QString());
|
|
|
|
|
+ m_bar->setFixedHeight(6);
|
|
|
|
|
+ m_bar->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QProgressBar { background:rgba(0,0,0,0.08); border:none; border-radius:3px; }"
|
|
|
|
|
+ "QProgressBar::chunk { background:#07C160; border-radius:3px; }"));
|
|
|
|
|
+
|
|
|
|
|
+ auto *meta = new QHBoxLayout();
|
|
|
|
|
+ meta->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ m_sizeLab = new QLabel(this);
|
|
|
|
|
+ m_sizeLab->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#888888; font-size:11px; background:transparent;"));
|
|
|
|
|
+ m_speedLab = new QLabel(this);
|
|
|
|
|
+ m_speedLab->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#888888; font-size:11px; background:transparent;"));
|
|
|
|
|
+ meta->addWidget(m_sizeLab, 1);
|
|
|
|
|
+ meta->addWidget(m_speedLab, 0, Qt::AlignRight);
|
|
|
|
|
+
|
|
|
|
|
+ root->addLayout(top);
|
|
|
|
|
+ root->addWidget(m_bar);
|
|
|
|
|
+ root->addLayout(meta);
|
|
|
|
|
+ m_bar->hide();
|
|
|
|
|
+ m_sizeLab->hide();
|
|
|
|
|
+ m_speedLab->hide();
|
|
|
|
|
+
|
|
|
|
|
+ connect(m_action, &QPushButton::clicked, this, [this]() { onAction(); });
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::statusChanged, this, [this](qint64 id) {
|
|
|
|
|
+ if (id == m_fileId)
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::progress, this,
|
|
|
|
|
+ [this](qint64 id, qint64, qint64, double) {
|
|
|
|
|
+ if (id == m_fileId)
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::failed, this,
|
|
|
|
|
+ [this](qint64 id, const QString &error) {
|
|
|
|
|
+ if (id != m_fileId)
|
|
|
|
|
+ return;
|
|
|
|
|
+ UiHelpers::showToast(window(),
|
|
|
|
|
+ error.isEmpty() ? QStringLiteral("下载失败") : error,
|
|
|
|
|
+ true);
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::ready, this, [this](qint64 id) {
|
|
|
|
|
+ if (id == m_fileId)
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ });
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected:
|
|
|
|
|
+ void mouseReleaseEvent(QMouseEvent *event) override
|
|
|
|
|
+ {
|
|
|
|
|
+ if (event->button() == Qt::LeftButton && rect().contains(event->pos()))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!m_action->geometry().contains(event->pos()))
|
|
|
|
|
+ onAction();
|
|
|
|
|
+ }
|
|
|
|
|
+ QWidget::mouseReleaseEvent(event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private:
|
|
|
|
|
+ void onAction()
|
|
|
|
|
+ {
|
|
|
|
|
+ const FileCache::Status st = FileCache::instance().status(m_fileId);
|
|
|
|
|
+ if (st == FileCache::Downloading)
|
|
|
|
|
+ {
|
|
|
|
|
+ FileCache::instance().cancelDownload(m_fileId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const QString path = FileCache::instance().localPath(m_fileId);
|
|
|
|
|
+ if (!path.isEmpty())
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!QDesktopServices::openUrl(QUrl::fromLocalFile(path)))
|
|
|
|
|
+ UiHelpers::showToast(window(), QStringLiteral("无法打开文件"), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ FileCache::instance().startDownload(m_fileId, m_url, m_mime, m_name, m_size);
|
|
|
|
|
+ syncUi();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void syncUi()
|
|
|
|
|
+ {
|
|
|
|
|
+ const FileCache::Status st = FileCache::instance().status(m_fileId);
|
|
|
|
|
+ qint64 rec = 0, tot = m_size;
|
|
|
|
|
+ double bps = 0;
|
|
|
|
|
+ FileCache::instance().progressOf(m_fileId, &rec, &tot, &bps);
|
|
|
|
|
+ if (tot <= 0)
|
|
|
|
|
+ tot = m_size;
|
|
|
|
|
+ const bool received = (st == FileCache::Received);
|
|
|
|
|
+ const bool transferring = (st == FileCache::Downloading);
|
|
|
|
|
+ const bool paused = (st == FileCache::NotReceived && rec > 0);
|
|
|
|
|
+
|
|
|
|
|
+ QString status = FileCache::statusText(st);
|
|
|
|
|
+ if (paused)
|
|
|
|
|
+ status = QStringLiteral("已暂停");
|
|
|
|
|
+ m_statusLab->setText(status);
|
|
|
|
|
+
|
|
|
|
|
+ if (transferring || paused)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_bar->show();
|
|
|
|
|
+ m_sizeLab->show();
|
|
|
|
|
+ m_speedLab->show();
|
|
|
|
|
+ if (tot > 0)
|
|
|
|
|
+ m_bar->setValue(
|
|
|
|
|
+ static_cast<int>(qBound(qint64(0), rec * 1000 / tot, qint64(1000))));
|
|
|
|
|
+ else
|
|
|
|
|
+ m_bar->setValue(0);
|
|
|
|
|
+ m_sizeLab->setText(QStringLiteral("%1 / %2")
|
|
|
|
|
+ .arg(FileCache::formatSize(rec),
|
|
|
|
|
+ tot > 0 ? FileCache::formatSize(tot)
|
|
|
|
|
+ : QStringLiteral("--")));
|
|
|
|
|
+ m_speedLab->setText(transferring ? FileCache::formatSpeed(bps)
|
|
|
|
|
+ : QStringLiteral("已暂停"));
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ m_bar->hide();
|
|
|
|
|
+ m_sizeLab->hide();
|
|
|
|
|
+ m_speedLab->hide();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (transferring)
|
|
|
|
|
+ m_action->setText(QStringLiteral("暂停"));
|
|
|
|
|
+ else if (received)
|
|
|
|
|
+ m_action->setText(QStringLiteral("打开"));
|
|
|
|
|
+ else
|
|
|
|
|
+ m_action->setText(QStringLiteral("下载"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ qint64 m_fileId = 0;
|
|
|
|
|
+ qint64 m_size = 0;
|
|
|
|
|
+ QString m_url;
|
|
|
|
|
+ QString m_mime;
|
|
|
|
|
+ QString m_name;
|
|
|
|
|
+ QLabel *m_statusLab = nullptr;
|
|
|
|
|
+ QProgressBar *m_bar = nullptr;
|
|
|
|
|
+ QLabel *m_sizeLab = nullptr;
|
|
|
|
|
+ QLabel *m_speedLab = nullptr;
|
|
|
|
|
+ QPushButton *m_action = nullptr;
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+FavoritePanel::FavoritePanel(QWidget *parent)
|
|
|
|
|
+ : QWidget(parent)
|
|
|
|
|
+{
|
|
|
|
|
+ setStyleSheet(QStringLiteral("background:#F5F5F5;"));
|
|
|
|
|
+ auto *lay = new QVBoxLayout(this);
|
|
|
|
|
+ lay->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ lay->setSpacing(0);
|
|
|
|
|
+
|
|
|
|
|
+ m_stack = new QStackedWidget(this);
|
|
|
|
|
+ m_list = new QListWidget(m_stack);
|
|
|
|
|
+ m_list->setFrameShape(QFrame::NoFrame);
|
|
|
|
|
+ m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
|
+ m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
|
|
|
+ m_list->setResizeMode(QListView::Adjust);
|
|
|
|
|
+ m_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
|
+ m_list->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QListWidget { background:#F5F5F5; border:none; outline:0; }"
|
|
|
|
|
+ "QListWidget::item { height:52px; padding:0; margin:0; border:none; }"
|
|
|
|
|
+ "QListWidget::item:hover { background:#EDEDED; }"
|
|
|
|
|
+ "QListWidget::item:selected { background:#C6C6C6; }"));
|
|
|
|
|
+ m_empty = new QLabel(QStringLiteral("暂无收藏\n可在聊天中右键收藏,或点右上角 + 新建笔记"), m_stack);
|
|
|
|
|
+ m_empty->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ m_empty->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#B0B0B0; font-size:13px; background:#F5F5F5; padding:24px;"));
|
|
|
|
|
+ m_stack->addWidget(m_list);
|
|
|
|
|
+ m_stack->addWidget(m_empty);
|
|
|
|
|
+ lay->addWidget(m_stack, 1);
|
|
|
|
|
+
|
|
|
|
|
+ connect(m_list, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) {
|
|
|
|
|
+ if (!item || (QApplication::mouseButtons() & Qt::RightButton))
|
|
|
|
|
+ return;
|
|
|
|
|
+ emit itemActivated(item->data(RoleId).toLongLong());
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(m_list, &QListWidget::customContextMenuRequested, this, &FavoritePanel::onMenu);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoritePanel::refresh()
|
|
|
|
|
+{
|
|
|
|
|
+ QPointer<FavoritePanel> self(this);
|
|
|
|
|
+ ImClient::instance().favoriteList([self](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self->m_list->clear();
|
|
|
|
|
+ const QJsonArray list = reply.value(QStringLiteral("list")).toArray();
|
|
|
|
|
+ for (const QJsonValue &v : list)
|
|
|
|
|
+ {
|
|
|
|
|
+ const QJsonObject o = v.toObject();
|
|
|
|
|
+ const qint64 id = Proto::jsonI64(o, QStringLiteral("id"));
|
|
|
|
|
+ const int kind = o.value(QStringLiteral("kind")).toInt();
|
|
|
|
|
+ const QString title = htmlToPlain(o.value(QStringLiteral("title")).toString());
|
|
|
|
|
+ const qint64 ts = Proto::jsonI64(o, QStringLiteral("ts"));
|
|
|
|
|
+ auto *item = new QListWidgetItem(self->m_list);
|
|
|
|
|
+ item->setData(RoleId, id);
|
|
|
|
|
+ item->setData(RoleKind, kind);
|
|
|
|
|
+ item->setData(RoleTitle, title);
|
|
|
|
|
+ item->setSizeHint(QSize(10, 52));
|
|
|
|
|
+ self->m_list->setItemWidget(item, makeRow(kind, title, ts, self->m_list));
|
|
|
|
|
+ }
|
|
|
|
|
+ self->applyFilter(QString());
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoritePanel::applyFilter(const QString &key)
|
|
|
|
|
+{
|
|
|
|
|
+ int visible = 0;
|
|
|
|
|
+ for (int i = 0; i < m_list->count(); ++i)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *item = m_list->item(i);
|
|
|
|
|
+ const bool hide = !key.isEmpty() &&
|
|
|
|
|
+ !item->data(RoleTitle).toString().contains(key, Qt::CaseInsensitive);
|
|
|
|
|
+ item->setHidden(hide);
|
|
|
|
|
+ if (!hide)
|
|
|
|
|
+ ++visible;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (visible == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ m_empty->setText(key.isEmpty()
|
|
|
|
|
+ ? QStringLiteral("暂无收藏\n可在聊天中右键收藏,或点右上角 + 新建笔记")
|
|
|
|
|
+ : QStringLiteral("未找到相关收藏"));
|
|
|
|
|
+ m_stack->setCurrentWidget(m_empty);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ m_stack->setCurrentWidget(m_list);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoritePanel::onMenu(const QPoint &pos)
|
|
|
|
|
+{
|
|
|
|
|
+ auto *item = m_list->itemAt(pos);
|
|
|
|
|
+ if (!item)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const qint64 id = item->data(RoleId).toLongLong();
|
|
|
|
|
+ QMenu menu;
|
|
|
|
|
+ QAction *del = menu.addAction(QStringLiteral("删除"));
|
|
|
|
|
+ if (menu.exec(m_list->viewport()->mapToGlobal(pos)) != del)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ConfirmDialog::ask(this, QStringLiteral("删除收藏"),
|
|
|
|
|
+ QStringLiteral("删除后不可恢复,仅影响你的收藏夹")))
|
|
|
|
|
+ return;
|
|
|
|
|
+ QPointer<FavoritePanel> self(this);
|
|
|
|
|
+ ImClient::instance().favoriteDelete(id, [self, id](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self->refresh();
|
|
|
|
|
+ emit self->itemDeleted(id);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+FavoriteDetailPage::FavoriteDetailPage(QWidget *parent)
|
|
|
|
|
+ : QWidget(parent)
|
|
|
|
|
+{
|
|
|
|
|
+ setStyleSheet(QStringLiteral("background:#EDEDED;"));
|
|
|
|
|
+ auto *lay = new QVBoxLayout(this);
|
|
|
|
|
+ lay->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
+ lay->setSpacing(0);
|
|
|
|
|
+
|
|
|
|
|
+ auto *header = new QWidget(this);
|
|
|
|
|
+ header->setFixedHeight(56);
|
|
|
|
|
+ header->setStyleSheet(QStringLiteral("background:#F7F7F7; border-bottom:1px solid #E5E5E5;"));
|
|
|
|
|
+ auto *hLay = new QHBoxLayout(header);
|
|
|
|
|
+ hLay->setContentsMargins(16, 0, 16, 0);
|
|
|
|
|
+ m_title = new QLabel(QStringLiteral("收藏"), header);
|
|
|
|
|
+ m_title->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "font-size:15px; font-weight:bold; background:transparent;"));
|
|
|
|
|
+ hLay->addWidget(m_title);
|
|
|
|
|
+ lay->addWidget(header);
|
|
|
|
|
+
|
|
|
|
|
+ m_toolbar = new QWidget(this);
|
|
|
|
|
+ m_toolbar->setStyleSheet(QStringLiteral("background:#F7F7F7; border-bottom:1px solid #E5E5E5;"));
|
|
|
|
|
+ auto *tLay = new QHBoxLayout(m_toolbar);
|
|
|
|
|
+ tLay->setContentsMargins(12, 6, 12, 6);
|
|
|
|
|
+ tLay->setSpacing(6);
|
|
|
|
|
+ auto makeTool = [this](const QString &text) {
|
|
|
|
|
+ auto *b = UiHelpers::ghostButton(text, m_toolbar);
|
|
|
|
|
+ b->setFixedHeight(26);
|
|
|
|
|
+ b->setStyleSheet(b->styleSheet() + QStringLiteral("QPushButton { font-size:12px; padding:3px 8px; }"));
|
|
|
|
|
+ return b;
|
|
|
|
|
+ };
|
|
|
|
|
+ auto makeIconTool = [this](int kind, const QString &tip) {
|
|
|
|
|
+ auto *b = new QPushButton(m_toolbar);
|
|
|
|
|
+ b->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ b->setFixedSize(28, 26);
|
|
|
|
|
+ b->setIconSize(QSize(16, 16));
|
|
|
|
|
+ b->setIcon(QIcon(toolIcon(kind, QColor("#1A1A1A"), 16)));
|
|
|
|
|
+ b->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QPushButton { background:#FFFFFF; border:1px solid #D9D9D9; border-radius:2px; padding:0; }"
|
|
|
|
|
+ "QPushButton:hover { background:#F5F5F5; }"));
|
|
|
|
|
+ return b;
|
|
|
|
|
+ };
|
|
|
|
|
+ auto *bold = makeTool(QStringLiteral("B"));
|
|
|
|
|
+ auto *italic = makeTool(QStringLiteral("I"));
|
|
|
|
|
+ auto *list = makeIconTool(0, QStringLiteral("列表"));
|
|
|
|
|
+ auto *img = makeIconTool(1, QStringLiteral("图片"));
|
|
|
|
|
+ auto *file = makeIconTool(2, QStringLiteral("文件"));
|
|
|
|
|
+ auto *save = UiHelpers::primaryButton(QStringLiteral("保存"), m_toolbar);
|
|
|
|
|
+ save->setFixedSize(72, 26);
|
|
|
|
|
+ save->setStyleSheet(save->styleSheet() + QStringLiteral(
|
|
|
|
|
+ "QPushButton { font-size:14px; padding:2px 10px; }"));
|
|
|
|
|
+ tLay->addWidget(bold);
|
|
|
|
|
+ tLay->addWidget(italic);
|
|
|
|
|
+ tLay->addWidget(list);
|
|
|
|
|
+ tLay->addWidget(img);
|
|
|
|
|
+ tLay->addWidget(file);
|
|
|
|
|
+ tLay->addStretch();
|
|
|
|
|
+ tLay->addWidget(save);
|
|
|
|
|
+ m_toolbar->hide();
|
|
|
|
|
+ lay->addWidget(m_toolbar);
|
|
|
|
|
+
|
|
|
|
|
+ m_body = new QWidget(this);
|
|
|
|
|
+ auto *bodyLay = new QVBoxLayout(m_body);
|
|
|
|
|
+ bodyLay->setContentsMargins(24, 24, 24, 24);
|
|
|
|
|
+ lay->addWidget(m_body, 1);
|
|
|
|
|
+
|
|
|
|
|
+ connect(bold, &QPushButton::clicked, this, [this]() {
|
|
|
|
|
+ if (!m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QTextCharFormat fmt;
|
|
|
|
|
+ fmt.setFontWeight(m_editor->fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
|
|
|
|
|
+ m_editor->mergeCurrentCharFormat(fmt);
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(italic, &QPushButton::clicked, this, [this]() {
|
|
|
|
|
+ if (!m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QTextCharFormat fmt;
|
|
|
|
|
+ fmt.setFontItalic(!m_editor->fontItalic());
|
|
|
|
|
+ m_editor->mergeCurrentCharFormat(fmt);
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(list, &QPushButton::clicked, this, [this]() {
|
|
|
|
|
+ if (!m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QTextCursor c = m_editor->textCursor();
|
|
|
|
|
+ QTextListFormat lf;
|
|
|
|
|
+ lf.setStyle(QTextListFormat::ListDisc);
|
|
|
|
|
+ c.createList(lf);
|
|
|
|
|
+ });
|
|
|
|
|
+ connect(img, &QPushButton::clicked, this, &FavoriteDetailPage::insertImage);
|
|
|
|
|
+ connect(file, &QPushButton::clicked, this, &FavoriteDetailPage::insertFile);
|
|
|
|
|
+ connect(save, &QPushButton::clicked, this, &FavoriteDetailPage::saveNote);
|
|
|
|
|
+
|
|
|
|
|
+ auto *saveSc = new QShortcut(QKeySequence::Save, this);
|
|
|
|
|
+ saveSc->setContext(Qt::WidgetWithChildrenShortcut);
|
|
|
|
|
+ connect(saveSc, &QShortcut::activated, this, [this]() {
|
|
|
|
|
+ if (m_editor && m_toolbar && m_toolbar->isVisible())
|
|
|
|
|
+ saveNote();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ showEmpty();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::clearBody()
|
|
|
|
|
+{
|
|
|
|
|
+ if (auto *lay = m_body->layout())
|
|
|
|
|
+ {
|
|
|
|
|
+ while (QLayoutItem *it = lay->takeAt(0))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (it->widget())
|
|
|
|
|
+ it->widget()->deleteLater();
|
|
|
|
|
+ delete it;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ m_editor = nullptr;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::showEmpty()
|
|
|
|
|
+{
|
|
|
|
|
+ m_id = 0;
|
|
|
|
|
+ m_kind = 0;
|
|
|
|
|
+ m_content.clear();
|
|
|
|
|
+ m_title->setText(QStringLiteral("收藏"));
|
|
|
|
|
+ m_toolbar->hide();
|
|
|
|
|
+ clearBody();
|
|
|
|
|
+ auto *hint = new QLabel(QStringLiteral("选择一条收藏,或点右上角 + 新建笔记"), m_body);
|
|
|
|
|
+ hint->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ hint->setStyleSheet(QStringLiteral("color:#B0B0B0; font-size:14px; background:transparent;"));
|
|
|
|
|
+ m_body->layout()->addWidget(hint);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::load(qint64 id)
|
|
|
|
|
+{
|
|
|
|
|
+ if (id <= 0)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QPointer<FavoriteDetailPage> self(this);
|
|
|
|
|
+ ImClient::instance().favoriteGet(id, [self](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self->render(reply);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::startNewNote()
|
|
|
|
|
+{
|
|
|
|
|
+ m_id = 0;
|
|
|
|
|
+ m_kind = im::FavKind::Note;
|
|
|
|
|
+ m_content.clear();
|
|
|
|
|
+ m_title->setText(QStringLiteral("新建笔记"));
|
|
|
|
|
+ renderNote(QJsonObject(), true);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::render(const QJsonObject &item)
|
|
|
|
|
+{
|
|
|
|
|
+ m_id = Proto::jsonI64(item, QStringLiteral("id"));
|
|
|
|
|
+ m_kind = item.value(QStringLiteral("kind")).toInt();
|
|
|
|
|
+ m_content = item.value(QStringLiteral("content")).toString();
|
|
|
|
|
+ const QString title = htmlToPlain(item.value(QStringLiteral("title")).toString());
|
|
|
|
|
+ m_title->setText(title.isEmpty() ? kindLabel(m_kind) : title);
|
|
|
|
|
+ m_toolbar->setVisible(m_kind == im::FavKind::Note);
|
|
|
|
|
+
|
|
|
|
|
+ if (m_kind == im::FavKind::Note)
|
|
|
|
|
+ {
|
|
|
|
|
+ renderNote(item, true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ clearBody();
|
|
|
|
|
+ auto *lay = static_cast<QVBoxLayout *>(m_body->layout());
|
|
|
|
|
+ if (m_kind == im::FavKind::Text)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *lab = new QLabel(m_content, m_body);
|
|
|
|
|
+ lab->setWordWrap(true);
|
|
|
|
|
+ lab->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
|
|
|
+ lab->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "color:#191919; font-size:14px; background:#FFFFFF; padding:12px; border-radius:4px;"));
|
|
|
|
|
+ lay->addWidget(lab);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const QJsonObject o = QJsonDocument::fromJson(m_content.toUtf8()).object();
|
|
|
|
|
+ if (m_kind == im::FavKind::Card)
|
|
|
|
|
+ {
|
|
|
|
|
+ const qint64 uid = Proto::jsonI64(o, QStringLiteral("uid"));
|
|
|
|
|
+ QString name = o.value(QStringLiteral("nickname")).toString();
|
|
|
|
|
+ const QString acc = o.value(QStringLiteral("account")).toString();
|
|
|
|
|
+ if (name.isEmpty())
|
|
|
|
|
+ name = acc;
|
|
|
|
|
+ const int ver = o.value(QStringLiteral("avatar_ver")).toInt();
|
|
|
|
|
+ auto *box = new QWidget(m_body);
|
|
|
|
|
+ box->setStyleSheet(QStringLiteral("background:#FFFFFF; border-radius:4px;"));
|
|
|
|
|
+ box->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ auto *h = new QHBoxLayout(box);
|
|
|
|
|
+ h->setContentsMargins(12, 10, 12, 10);
|
|
|
|
|
+ auto *av = new QLabel(box);
|
|
|
|
|
+ av->setFixedSize(48, 48);
|
|
|
|
|
+ av->setPixmap(AvatarCache::instance().pixmap(uid, ver, name, 48));
|
|
|
|
|
+ auto *info = new QVBoxLayout();
|
|
|
|
|
+ auto *n = new QLabel(name, box);
|
|
|
|
|
+ n->setStyleSheet(QStringLiteral("color:#191919; font-size:14px; background:transparent;"));
|
|
|
|
|
+ auto *a = new QLabel(QStringLiteral("账号 %1").arg(acc), box);
|
|
|
|
|
+ a->setStyleSheet(QStringLiteral("color:#888; font-size:12px; background:transparent;"));
|
|
|
|
|
+ info->addWidget(n);
|
|
|
|
|
+ info->addWidget(a);
|
|
|
|
|
+ h->addWidget(av);
|
|
|
|
|
+ h->addLayout(info, 1);
|
|
|
|
|
+ lay->addWidget(box, 0, Qt::AlignLeft);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ box->setObjectName(QStringLiteral("favCard"));
|
|
|
|
|
+ box->setProperty("cardUid", uid);
|
|
|
|
|
+ box->installEventFilter(this);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const qint64 fileId = Proto::jsonI64(o, QStringLiteral("file_id"));
|
|
|
|
|
+ const QString name = o.value(QStringLiteral("name")).toString();
|
|
|
|
|
+ const QString mime = o.value(QStringLiteral("mime")).toString();
|
|
|
|
|
+ const QString url = o.value(QStringLiteral("url")).toString();
|
|
|
|
|
+ const qint64 size = Proto::jsonI64(o, QStringLiteral("size"));
|
|
|
|
|
+ FileCache::instance().bindMeta(fileId, url, mime, name, size);
|
|
|
|
|
+
|
|
|
|
|
+ if (m_kind == im::FavKind::Image)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *img = new QLabel(m_body);
|
|
|
|
|
+ img->setAlignment(Qt::AlignCenter);
|
|
|
|
|
+ img->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
+ img->setStyleSheet(QStringLiteral("background:#FFFFFF; border-radius:4px;"));
|
|
|
|
|
+ const QPixmap pm = FileCache::instance().thumbnail(fileId, 320);
|
|
|
|
|
+ if (pm.isNull())
|
|
|
|
|
+ {
|
|
|
|
|
+ img->setFixedSize(180, 120);
|
|
|
|
|
+ img->setText(QStringLiteral("加载中…"));
|
|
|
|
|
+ FileCache::instance().ensure(fileId, url, mime, name);
|
|
|
|
|
+ QPointer<QLabel> imgRef(img);
|
|
|
|
|
+ QPointer<FavoriteDetailPage> self(this);
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::ready, img, [imgRef, self, fileId, name, o](qint64 id) {
|
|
|
|
|
+ if (!imgRef || id != fileId)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QPixmap got = FileCache::instance().thumbnail(fileId, 320);
|
|
|
|
|
+ if (got.isNull())
|
|
|
|
|
+ return;
|
|
|
|
|
+ imgRef->setText(QString());
|
|
|
|
|
+ imgRef->setPixmap(got);
|
|
|
|
|
+ const qreal dpr = qMax(1.0, got.devicePixelRatio());
|
|
|
|
|
+ imgRef->setFixedSize(qRound(got.width() / dpr), qRound(got.height() / dpr));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ img->setPixmap(pm);
|
|
|
|
|
+ const qreal dpr = qMax(1.0, pm.devicePixelRatio());
|
|
|
|
|
+ img->setFixedSize(qRound(pm.width() / dpr), qRound(pm.height() / dpr));
|
|
|
|
|
+ }
|
|
|
|
|
+ img->installEventFilter(this);
|
|
|
|
|
+ img->setProperty("fileId", fileId);
|
|
|
|
|
+ img->setProperty("fileName", name);
|
|
|
|
|
+ lay->addWidget(img, 0, Qt::AlignLeft);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto *card = new FavFileCard(m_body, fileId, name, size, url, mime);
|
|
|
|
|
+ lay->addWidget(card, 0, Qt::AlignLeft);
|
|
|
|
|
+ lay->addStretch();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool FavoriteDetailPage::eventFilter(QObject *watched, QEvent *event)
|
|
|
|
|
+{
|
|
|
|
|
+ if (event->type() == QEvent::KeyPress)
|
|
|
|
|
+ {
|
|
|
|
|
+ auto *key = static_cast<QKeyEvent *>(event);
|
|
|
|
|
+ if (key->matches(QKeySequence::Save))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (m_editor && m_toolbar && m_toolbar->isVisible())
|
|
|
|
|
+ saveNote();
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (event->type() == QEvent::MouseButtonRelease)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (auto *w = qobject_cast<QWidget *>(watched))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (w->objectName() == QStringLiteral("favCard"))
|
|
|
|
|
+ {
|
|
|
|
|
+ emit openCard(w->property("cardUid").toLongLong());
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ const qint64 fileId = w->property("fileId").toLongLong();
|
|
|
|
|
+ if (fileId > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ emit previewImage(fileId, w->property("fileName").toString(), m_content);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return QWidget::eventFilter(watched, event);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::renderNote(const QJsonObject &item, bool editing)
|
|
|
|
|
+{
|
|
|
|
|
+ Q_UNUSED(editing);
|
|
|
|
|
+ clearBody();
|
|
|
|
|
+ m_toolbar->show();
|
|
|
|
|
+ auto *lay = static_cast<QVBoxLayout *>(m_body->layout());
|
|
|
|
|
+ m_editor = new QTextEdit(m_body);
|
|
|
|
|
+ m_editor->setAcceptRichText(true);
|
|
|
|
|
+ m_editor->installEventFilter(this);
|
|
|
|
|
+ m_editor->setStyleSheet(QStringLiteral(
|
|
|
|
|
+ "QTextEdit { background:#FFFFFF; border:1px solid #E5E5E5; padding:8px; }"));
|
|
|
|
|
+ QString html;
|
|
|
|
|
+ const QString raw = item.value(QStringLiteral("content")).toString();
|
|
|
|
|
+ const QJsonObject note = QJsonDocument::fromJson(raw.toUtf8()).object();
|
|
|
|
|
+ html = note.value(QStringLiteral("html")).toString();
|
|
|
|
|
+ if (html.isEmpty())
|
|
|
|
|
+ html = raw;
|
|
|
|
|
+ if (!html.isEmpty())
|
|
|
|
|
+ m_editor->setHtml(html);
|
|
|
|
|
+ lay->addWidget(m_editor, 1);
|
|
|
|
|
+ bindNoteImages(m_editor->toHtml());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::bindNoteImages(const QString &html)
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QVector<qint64> ids = favFileIds(html);
|
|
|
|
|
+ QPointer<QTextEdit> ed(m_editor);
|
|
|
|
|
+ for (qint64 id : ids)
|
|
|
|
|
+ {
|
|
|
|
|
+ const QPixmap pm = FileCache::instance().thumbnail(id, 240);
|
|
|
|
|
+ if (!pm.isNull())
|
|
|
|
|
+ {
|
|
|
|
|
+ m_editor->document()->addResource(QTextDocument::ImageResource,
|
|
|
|
|
+ QUrl(QStringLiteral("favfile:%1").arg(id)), pm);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ FileCache::instance().ensure(id);
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::ready, m_editor,
|
|
|
|
|
+ [ed, id](qint64 got) {
|
|
|
|
|
+ if (!ed || got != id)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QPixmap p = FileCache::instance().thumbnail(id, 240);
|
|
|
|
|
+ if (p.isNull())
|
|
|
|
|
+ return;
|
|
|
|
|
+ ed->document()->addResource(QTextDocument::ImageResource,
|
|
|
|
|
+ QUrl(QStringLiteral("favfile:%1").arg(id)), p);
|
|
|
|
|
+ ed->viewport()->update();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+QString FavoriteDetailPage::noteContentJson() const
|
|
|
|
|
+{
|
|
|
|
|
+ QJsonObject o;
|
|
|
|
|
+ const QString html = m_editor ? m_editor->toHtml() : QString();
|
|
|
|
|
+ o.insert(QStringLiteral("html"), html);
|
|
|
|
|
+ QJsonArray files;
|
|
|
|
|
+ for (qint64 id : favFileIds(html))
|
|
|
|
|
+ files.append(id);
|
|
|
|
|
+ o.insert(QStringLiteral("files"), files);
|
|
|
|
|
+ return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::saveNote()
|
|
|
|
|
+{
|
|
|
|
|
+ if (!m_editor || !m_toolbar || !m_toolbar->isVisible())
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QString content = noteContentJson();
|
|
|
|
|
+ QPointer<FavoriteDetailPage> self(this);
|
|
|
|
|
+ auto done = [self](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ self->m_id = Proto::jsonI64(reply, QStringLiteral("id"));
|
|
|
|
|
+ UiHelpers::showToast(self, QStringLiteral("已保存"));
|
|
|
|
|
+ emit self->changed();
|
|
|
|
|
+ };
|
|
|
|
|
+ if (m_id > 0)
|
|
|
|
|
+ ImClient::instance().favoriteUpdate(m_id, content, done);
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ QJsonObject body;
|
|
|
|
|
+ body.insert(QStringLiteral("kind"), im::FavKind::Note);
|
|
|
|
|
+ body.insert(QStringLiteral("content"), content);
|
|
|
|
|
+ ImClient::instance().favoriteAdd(body, done);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::insertImage()
|
|
|
|
|
+{
|
|
|
|
|
+ uploadAndInsert(QString(), true);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::insertFile()
|
|
|
|
|
+{
|
|
|
|
|
+ uploadAndInsert(QString(), false);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void FavoriteDetailPage::uploadAndInsert(const QString &forcedPath, bool asImage)
|
|
|
|
|
+{
|
|
|
|
|
+ Q_UNUSED(forcedPath);
|
|
|
|
|
+ if (!m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const QString filter = asImage
|
|
|
|
|
+ ? QStringLiteral("图片 (*.jpg *.jpeg *.png *.gif *.bmp *.webp)")
|
|
|
|
|
+ : QString();
|
|
|
|
|
+ const QString path = QFileDialog::getOpenFileName(
|
|
|
|
|
+ this, asImage ? QStringLiteral("选择图片") : QStringLiteral("选择文件"), QString(), filter);
|
|
|
|
|
+ if (path.isEmpty())
|
|
|
|
|
+ return;
|
|
|
|
|
+ QFileInfo info(path);
|
|
|
|
|
+ if (info.size() <= 0)
|
|
|
|
|
+ return;
|
|
|
|
|
+ const qint64 fileMax = AppConfig::instance().fileMax();
|
|
|
|
|
+ if (fileMax > 0 && info.size() > fileMax)
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(this, QStringLiteral("文件过大"), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const QString mime = QMimeDatabase().mimeTypeForFile(path).name();
|
|
|
|
|
+ const QString name = info.fileName();
|
|
|
|
|
+ QPointer<FavoriteDetailPage> self(this);
|
|
|
|
|
+ ImClient::instance().putFile(name, mime, info.size(), QStringLiteral("favorite"),
|
|
|
|
|
+ [self, path, name, mime, asImage](const QJsonObject &reply) {
|
|
|
|
|
+ if (!self || !self->m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ if (!ImClient::ok(reply))
|
|
|
|
|
+ {
|
|
|
|
|
+ UiHelpers::showToast(self, ImClient::messageOf(reply), true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const qint64 fileId = Proto::jsonI64(reply, QStringLiteral("file_id"));
|
|
|
|
|
+ const QString url = reply.value(QStringLiteral("url")).toString();
|
|
|
|
|
+ if (fileId <= 0)
|
|
|
|
|
+ return;
|
|
|
|
|
+ FileCache::instance().rememberPath(fileId, path, mime, name);
|
|
|
|
|
+ FileCache::instance().bindMeta(fileId, url, mime, name, QFileInfo(path).size());
|
|
|
|
|
+ auto *once = new QObject(self);
|
|
|
|
|
+ connect(&FileCache::instance(), &FileCache::uploaded, once,
|
|
|
|
|
+ [self, once, fileId, name, asImage](qint64 id) {
|
|
|
|
|
+ if (id != fileId)
|
|
|
|
|
+ return;
|
|
|
|
|
+ once->deleteLater();
|
|
|
|
|
+ if (!self || !self->m_editor)
|
|
|
|
|
+ return;
|
|
|
|
|
+ QTextCursor c = self->m_editor->textCursor();
|
|
|
|
|
+ if (asImage)
|
|
|
|
|
+ {
|
|
|
|
|
+ const QPixmap pm = FileCache::instance().thumbnail(fileId, 240);
|
|
|
|
|
+ const QString key = QStringLiteral("favfile:%1").arg(fileId);
|
|
|
|
|
+ if (!pm.isNull())
|
|
|
|
|
+ self->m_editor->document()->addResource(
|
|
|
|
|
+ QTextDocument::ImageResource, QUrl(key), pm);
|
|
|
|
|
+ QTextImageFormat fmt;
|
|
|
|
|
+ fmt.setName(key);
|
|
|
|
|
+ fmt.setWidth(180);
|
|
|
|
|
+ c.insertImage(fmt);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ c.insertHtml(QStringLiteral("<p><a href=\"favfile:%1\">%2</a></p>")
|
|
|
|
|
+ .arg(fileId).arg(name.toHtmlEscaped()));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ FileCache::instance().startUpload(fileId, reply.value(QStringLiteral("token")).toObject());
|
|
|
|
|
+ });
|
|
|
|
|
+}
|