4#include <QLoggingCategory>
12#include <QProgressBar>
14#include <QStandardItemModel>
15#include <QNetworkAccessManager>
16#include <QNetworkRequest>
17#include <QNetworkReply>
19#include <QJsonDocument>
27#include <QDesktopServices>
28#include <QStandardPaths>
29#include <QCryptographicHash>
31#include "FrmExtensionStore.h"
32#include "FrmExtensionManager.h"
33#include "ParameterWebBrowser.h"
34#include "ui_FrmExtensionStore.h"
36static Q_LOGGING_CATEGORY(log,
"WebBrowser.ExtensionStore")
39const QString DEFAULT_STORE_API = "https:
44 , m_pNetworkManager(
nullptr)
45 , m_pExtensionManager(
nullptr)
47 , m_pModelExtensions(
nullptr)
48 , m_apiBaseUrl(DEFAULT_STORE_API)
51 setWindowTitle(tr(
"Chrome Extension Store"));
52 setWindowIcon(QIcon::fromTheme(
"store"));
55 SetupNetworkManager();
58 qDebug(log) << Q_FUNC_INFO <<
"Extension store initialized";
61CFrmExtensionStore::~CFrmExtensionStore()
63 qDebug(log) << Q_FUNC_INFO;
66 for(
auto reply : m_downloads) {
76void CFrmExtensionStore::InitializeUI()
79 m_pModelExtensions =
new QStandardItemModel(
this);
80 m_pModelExtensions->setColumnCount(7);
84 headers << tr(
"Icon") << tr(
"Name") << tr(
"Version")
85 << tr(
"Rating") << tr(
"Downloads") << tr(
"ID") << tr(
"Status");
86 m_pModelExtensions->setHorizontalHeaderLabels(headers);
89 ui->tvExtensions->setModel(m_pModelExtensions);
90 ui->tvExtensions->setSelectionMode(QAbstractItemView::SingleSelection);
91 ui->tvExtensions->setSelectionBehavior(QAbstractItemView::SelectRows);
92 ui->tvExtensions->setEditTriggers(QAbstractItemView::NoEditTriggers);
93 ui->tvExtensions->setContextMenuPolicy(Qt::CustomContextMenu);
94 ui->tvExtensions->setAlternatingRowColors(
true);
97 ui->tvExtensions->horizontalHeader()->setStretchLastSection(
true);
98 ui->tvExtensions->verticalHeader()->setDefaultSectionSize(24);
101 ui->leSearch->setPlaceholderText(tr(
"Search extensions..."));
104 ui->pbSearch->setText(tr(
"Search"));
105 ui->pbPopular->setText(tr(
"Popular"));
106 ui->pbRecommended->setText(tr(
"Recommended"));
107 ui->pbDownload->setText(tr(
"Download"));
108 ui->pbInstall->setText(tr(
"Install"));
109 ui->pbCancel->setText(tr(
"Cancel"));
110 ui->pbDetails->setText(tr(
"Details"));
111 ui->pbRefresh->setText(tr(
"Refresh"));
112 ui->pbClearCache->setText(tr(
"Clear Cache"));
115 ui->lblExtensionName->setText(tr(
"Extension Name"));
116 ui->lblExtensionDesc->setWordWrap(
true);
119 ui->progressDownload->setVisible(
false);
121 qDebug(log) <<
"UI initialized";
124void CFrmExtensionStore::SetupNetworkManager()
126 m_pNetworkManager =
new QNetworkAccessManager(
this);
127 qDebug(log) <<
"Network manager setup completed";
130void CFrmExtensionStore::SetupConnections()
135 check = connect(ui->pbSearch, &QPushButton::clicked,
136 this, &CFrmExtensionStore::on_pbSearch_clicked);
137 check = connect(ui->pbPopular, &QPushButton::clicked,
138 this, &CFrmExtensionStore::on_pbPopular_clicked);
139 check = connect(ui->pbRecommended, &QPushButton::clicked,
140 this, &CFrmExtensionStore::on_pbRecommended_clicked);
141 check = connect(ui->pbDownload, &QPushButton::clicked,
142 this, &CFrmExtensionStore::on_pbDownload_clicked);
143 check = connect(ui->pbInstall, &QPushButton::clicked,
144 this, &CFrmExtensionStore::on_pbInstall_clicked);
145 check = connect(ui->pbCancel, &QPushButton::clicked,
146 this, &CFrmExtensionStore::on_pbCancel_clicked);
147 check = connect(ui->pbDetails, &QPushButton::clicked,
148 this, &CFrmExtensionStore::on_pbDetails_clicked);
149 check = connect(ui->pbRefresh, &QPushButton::clicked,
150 this, &CFrmExtensionStore::on_pbRefresh_clicked);
151 check = connect(ui->pbClearCache, &QPushButton::clicked,
152 this, &CFrmExtensionStore::on_pbClearCache_clicked);
155 check = connect(ui->tvExtensions->selectionModel(), &QItemSelectionModel::selectionChanged,
156 this, &CFrmExtensionStore::slotExtensionSelected);
157 check = connect(ui->tvExtensions, &QTableView::customContextMenuRequested,
158 this, &CFrmExtensionStore::slotCustomContextMenu);
161 check = connect(ui->leSearch, &QLineEdit::returnPressed,
162 this, &CFrmExtensionStore::on_pbSearch_clicked);
165 qDebug(log) <<
"Connections setup completed";
170 m_pExtensionManager = manager;
171 if(!m_pExtensionManager) {
172 qWarning(log) <<
"Failed to set extension manager: manager is null";
176 qDebug(log) <<
"Extension manager set successfully";
182 m_apiBaseUrl = baseUrl;
183 qDebug(log) <<
"API base URL set to:" << m_apiBaseUrl;
188 qDebug(log) << Q_FUNC_INFO <<
"Keyword:" << keyword;
190 if(!m_pNetworkManager)
return;
193 QString searchUrl = m_apiBaseUrl +
"/search?q=" + keyword;
195 QNetworkRequest request(QUrl(searchUrl));
224 qDebug(log) << Q_FUNC_INFO;
226 if(!m_pNetworkManager)
return;
228 QString url = m_apiBaseUrl +
"/popular";
255 qDebug(log) << Q_FUNC_INFO;
257 if(!m_pNetworkManager)
return;
259 QString url = m_apiBaseUrl +
"/recommended";
286 qDebug(log) << Q_FUNC_INFO <<
"Extension ID:" << extensionId;
288 if(!m_pNetworkManager)
return QString();
291 QString downloadId = GenerateDownloadId();
294 QString downloadUrl = GetExtensionFileUrl(extensionId);
342 qDebug(log) << Q_FUNC_INFO <<
"Download ID:" << downloadId;
344 if(m_downloads.contains(downloadId)) {
345 QNetworkReply* reply = m_downloads[downloadId];
348 reply->deleteLater();
350 m_downloads.remove(downloadId);
351 m_downloadExtensionId.remove(downloadId);
361void CFrmExtensionStore::on_pbSearch_clicked()
363 QString keyword = ui->leSearch->text();
364 if(keyword.isEmpty()) {
365 QMessageBox::warning(
this, tr(
"Warning"), tr(
"Please enter a search keyword"));
372void CFrmExtensionStore::on_pbPopular_clicked()
377void CFrmExtensionStore::on_pbRecommended_clicked()
382void CFrmExtensionStore::on_pbDownload_clicked()
384 auto indexes = ui->tvExtensions->selectionModel()->selectedRows();
385 if(indexes.isEmpty()) {
386 QMessageBox::warning(
this, tr(
"Warning"), tr(
"Please select an extension"));
390 int row = indexes.at(0).row();
391 auto idItem = m_pModelExtensions->item(row, ColumnNo::ID);
392 auto nameItem = m_pModelExtensions->item(row, ColumnNo::Name);
394 if(!idItem || !nameItem)
return;
396 QString extensionId = idItem->text();
398 int ret = QMessageBox::question(
this, tr(
"Confirm"),
399 tr(
"Do you want to download '%1'?").arg(nameItem->text()),
400 QMessageBox::Yes | QMessageBox::No);
402 if(ret == QMessageBox::Yes) {
404 if(!downloadId.isEmpty()) {
405 ui->progressDownload->setVisible(
true);
406 ui->progressDownload->setValue(0);
411void CFrmExtensionStore::on_pbInstall_clicked()
413 qDebug(log) << Q_FUNC_INFO;
415 if(!m_pExtensionManager) {
416 QMessageBox::warning(
this, tr(
"Error"),
417 tr(
"Extension manager not set"));
422 QString filter = tr(
"Chrome Extension (*.crx);;Extension Folder;;All Files (*)");
423 QString path = QFileDialog::getOpenFileName(
424 this, tr(
"Select Downloaded Extension"), GetDownloadPath(), filter);
426 if(!path.isEmpty()) {
431void CFrmExtensionStore::on_pbCancel_clicked()
433 auto indexes = ui->tvExtensions->selectionModel()->selectedRows();
434 if(indexes.isEmpty())
return;
437 for(
auto downloadId : m_downloads.keys()) {
441 ui->progressDownload->setVisible(
false);
442 QMessageBox::information(
this, tr(
"Info"), tr(
"All downloads cancelled"));
445void CFrmExtensionStore::on_pbDetails_clicked()
447 qDebug(log) << Q_FUNC_INFO;
449 auto indexes = ui->tvExtensions->selectionModel()->selectedRows();
450 if(indexes.isEmpty())
return;
452 int row = indexes.at(0).row();
453 auto idItem = m_pModelExtensions->item(row, ColumnNo::ID);
457 QString extensionId = idItem->text();
459 if(m_extensionCache.contains(extensionId)) {
460 DisplayExtensionDetails(m_extensionCache[extensionId]);
464void CFrmExtensionStore::on_pbRefresh_clicked()
469void CFrmExtensionStore::on_pbClearCache_clicked()
471 qDebug(log) << Q_FUNC_INFO;
473 QString cachePath = GetCachePath();
477 dir.removeRecursively();
478 qDebug(log) <<
"Cache cleared:" << cachePath;
479 QMessageBox::information(
this, tr(
"Success"), tr(
"Cache cleared"));
483void CFrmExtensionStore::slotExtensionSelected()
485 auto indexes = ui->tvExtensions->selectionModel()->selectedRows();
486 if(indexes.isEmpty())
return;
488 int row = indexes.at(0).row();
489 auto idItem = m_pModelExtensions->item(row, ColumnNo::ID);
491 if(idItem && m_extensionCache.contains(idItem->text())) {
492 DisplayExtensionDetails(m_extensionCache[idItem->text()]);
496void CFrmExtensionStore::slotCustomContextMenu(
const QPoint &pos)
498 qDebug(log) << Q_FUNC_INFO;
502void CFrmExtensionStore::slotDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
505 int progress = (bytesReceived * 100) / bytesTotal;
506 ui->progressDownload->setValue(progress);
508 qDebug(log) <<
"Download progress:" << progress <<
"%";
512void CFrmExtensionStore::slotSearchFinished()
514 qDebug(log) << Q_FUNC_INFO;
517void CFrmExtensionStore::slotDownloadFinished()
519 qDebug(log) << Q_FUNC_INFO;
522void CFrmExtensionStore::slotNetworkError(QNetworkReply::NetworkError error)
524 qWarning(log) <<
"Network error:" << error;
525 QMessageBox::warning(
this, tr(
"Network Error"),
526 tr(
"Failed to connect to extension store"));
529void CFrmExtensionStore::RefreshExtensionList()
534int CFrmExtensionStore::AddExtensionItem(
const QJsonObject &extInfo)
536 if(!m_pModelExtensions)
return -1;
538 QString extensionId = extInfo.value(
"id").toString();
539 QString name = extInfo.value(
"name").toString();
540 QString version = extInfo.value(
"version").toString(
"1.0.0");
541 double rating = extInfo.value(
"rating").toDouble(4.5);
542 int downloads = extInfo.value(
"downloads").toInt(0);
545 m_extensionCache[extensionId] = extInfo;
547 int row = m_pModelExtensions->rowCount();
548 m_pModelExtensions->insertRow(row);
551 auto itemIcon =
new QStandardItem();
552 itemIcon->setEditable(
false);
553 m_pModelExtensions->setItem(row, ColumnNo::Icon, itemIcon);
556 auto itemName =
new QStandardItem(name);
557 itemName->setEditable(
false);
558 m_pModelExtensions->setItem(row, ColumnNo::Name, itemName);
561 auto itemVersion =
new QStandardItem(version);
562 itemVersion->setEditable(
false);
563 m_pModelExtensions->setItem(row, ColumnNo::Version, itemVersion);
566 auto itemRating =
new QStandardItem(QString::number(rating,
'f', 1));
567 itemRating->setEditable(
false);
568 m_pModelExtensions->setItem(row, ColumnNo::Rating, itemRating);
571 auto itemDownloads =
new QStandardItem(QString::number(downloads));
572 itemDownloads->setEditable(
false);
573 m_pModelExtensions->setItem(row, ColumnNo::Downloads, itemDownloads);
576 auto itemId =
new QStandardItem(extensionId);
577 itemId->setEditable(
false);
578 m_pModelExtensions->setItem(row, ColumnNo::ID, itemId);
581 QString status = tr(
"Not Installed");
582 if(m_pExtensionManager) {
584 foreach (
auto e, installed) {
585 if(e.id() == extensionId)
586 status = tr(
"Installed");
590 auto itemStatus =
new QStandardItem(status);
591 itemStatus->setEditable(
false);
592 m_pModelExtensions->setItem(row, ColumnNo::Status, itemStatus);
594 qDebug(log) <<
"Added extension:" << name <<
"ID:" << extensionId;
598void CFrmExtensionStore::DisplayExtensionDetails(
const QJsonObject &extInfo)
600 QString name = extInfo.value(
"name").toString();
601 QString description = extInfo.value(
"description").toString();
602 QString version = extInfo.value(
"version").toString();
603 double rating = extInfo.value(
"rating").toDouble(4.5);
604 int downloads = extInfo.value(
"downloads").toInt(0);
605 QString author = extInfo.value(
"author").toString();
607 ui->lblExtensionName->setText(name);
608 ui->lblExtensionDesc->setText(description);
610 QString details = QString(
616 ).arg(name, version, QString::number(rating,
'f', 1),
617 QString::number(downloads), author);
619 ui->lblExtensionDesc->setText(details);
622void CFrmExtensionStore::ClearExtensionList()
624 if(m_pModelExtensions) {
625 m_pModelExtensions->removeRows(0, m_pModelExtensions->rowCount());
629int CFrmExtensionStore::ProcessDownloadedFile(
const QString &filePath,
630 const QString &extensionId)
632 qDebug(log) << Q_FUNC_INFO <<
"File:" << filePath;
634 if(!m_pExtensionManager)
return -1;
637 int ret = QMessageBox::question(
this, tr(
"Install Now?"),
638 tr(
"Do you want to install this extension now?"),
639 QMessageBox::Yes | QMessageBox::No);
641 if(ret == QMessageBox::Yes) {
648QString CFrmExtensionStore::GetDownloadPath()
const
650 QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)
651 +
"/RabbitRemoteControl/Extensions";
654 if(!dir.exists(path)) {
661QString CFrmExtensionStore::GetCachePath()
const
663 QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
667 if(!dir.exists(path)) {
674int CFrmExtensionStore::SaveToCache(
const QString &key,
const QJsonObject &data)
676 QString cachePath = GetCachePath() +
"/" + key +
".json";
678 QJsonDocument doc(data);
679 QFile file(cachePath);
681 if(file.open(QIODevice::WriteOnly)) {
682 file.write(doc.toJson());
684 qDebug(log) <<
"Cached:" << key;
691QJsonObject CFrmExtensionStore::LoadFromCache(
const QString &key)
const
693 QString cachePath = GetCachePath() +
"/" + key +
".json";
695 QFile file(cachePath);
696 if(file.open(QIODevice::ReadOnly)) {
697 QByteArray data = file.readAll();
700 QJsonDocument doc = QJsonDocument::fromJson(data);
704 return QJsonObject();
707bool CFrmExtensionStore::IsCacheValid(
const QString &key)
const
709 QString cachePath = GetCachePath() +
"/" + key +
".json";
711 QFile file(cachePath);
712 if(!file.exists())
return false;
714 QFileInfo fi(cachePath);
715 QDateTime lastModified = fi.lastModified();
716 QDateTime now = QDateTime::currentDateTime();
718 int hours = lastModified.secsTo(now) / 3600;
719 return hours < CACHE_VALIDITY_HOURS;
722QJsonArray CFrmExtensionStore::ParseExtensionList(
const QByteArray &data)
const
724 QJsonDocument doc = QJsonDocument::fromJson(data);
728 }
else if(doc.isObject()) {
729 QJsonObject obj = doc.object();
730 if(obj.contains(
"extensions")) {
731 return obj.value(
"extensions").toArray();
738QJsonObject CFrmExtensionStore::ParseExtensionDetails(
const QByteArray &data)
const
740 QJsonDocument doc = QJsonDocument::fromJson(data);
744QString CFrmExtensionStore::GenerateDownloadId()
const
746 return QUuid::createUuid().toString(QUuid::WithoutBraces);
749bool CFrmExtensionStore::IsExtensionIdValid(
const QString &
id)
const
751 return !
id.isEmpty() &&
id.length() >= 32;
754QString CFrmExtensionStore::GetExtensionFileUrl(
const QString &extensionId)
const
757 return m_apiBaseUrl +
"/extensions/" + extensionId +
".crx";
760QString CFrmExtensionStore::GetChromeWebStoreUrl(
const QString &extensionId)
const
762 return "https://chromewebstore.google.com/detail/" + extensionId;
QList< QWebEngineExtensionInfo > GetInstalledExtensions() const
获取已安装的扩展列表
void InstallExtension(const QString &path)
安装或加载 Chrome 扩展
void CancelDownload(const QString &downloadId)
取消下载
void SearchExtensions(const QString &keyword)
搜索扩展
QString DownloadExtension(const QString &extensionId)
下载扩展
int GetDownloadProgress(const QString &downloadId) const
获取下载进度
void GetRecommendedExtensions()
获取推荐扩展列表
int SetExtensionManager(CFrmExtensionManager *manager)
设置扩展管理器
void SetAPIBaseUrl(const QString &baseUrl)
设置基础 URL (可以指向自建服务器)
void GetPopularExtensions()
获取热门扩展列表