#include #include #include #include "spkmsgbox.h" #include "spkmainwindow.h" #include "spklogging.h" #include "spkutils.h" #include "spkuimsg.h" SpkMainWindow::SpkMainWindow(QWidget *parent) : SpkWindow(parent) { ui = new SpkUi::SpkMainWidget(parent); Initialize(); SetUseTitleBar(false); SetCentralWidget(ui); SetTitleBar(ui->TitleBar, false); RefreshCategoryData(); auto size = QGuiApplication::primaryScreen()->size() * 0.25; resize(QGuiApplication::primaryScreen()->size() * 0.5); move(size.width(), size.height()); } void SpkMainWindow::SwitchDayNightTheme() { if(SpkUi::CurrentStyle == SpkUi::Dark) SpkUi::SetGlobalStyle(SpkUi::Light, true); else SpkUi::SetGlobalStyle(SpkUi::Dark, true); ReloadThemedUiIcons(); } void SpkMainWindow::SwitchToPage(SpkUi::SpkStackedPages page) { if(mCurrentPage != page) { ui->Pager->setCurrentIndex(int(page)); mCurrentPage = page; // If the page is a SpkPageBase (with a resource context), activate it for resource acquisition auto tryActivate = qobject_cast(ui->Pager->currentWidget()); if(tryActivate) tryActivate->Activated(); } } void SpkMainWindow::PopulateCategories(QJsonArray aCategoryData) { using SpkUi::SpkSidebarSelector; QTreeWidgetItem *catg; if(ui->CategoryParentItem->childCount()) // Clear all existing children if there is any foreach(auto &i, ui->CategoryParentItem->takeChildren()) delete i; foreach(auto i, aCategoryData) { if(i.isObject()) { auto j = i.toObject(); double typeId; QString typeName; if(j.contains("type_id") && j.value("type_id").isDouble()) typeId = j.value("type_id").toDouble(); else goto WRONG_CATEGORY; if(j.contains("type_name") && j.value("type_name").isString()) typeName = j.value("type_name").toString(); else goto WRONG_CATEGORY; catg = new QTreeWidgetItem(ui->CategoryParentItem, QStringList(typeName)); catg->setData(0, SpkSidebarSelector::RoleItemIsCategory, true); catg->setData(0, SpkSidebarSelector::RoleItemCategoryPageId, typeId); continue; WRONG_CATEGORY:; } ui->CategoryParentItem->setExpanded(true); } } void SpkMainWindow::RefreshCategoryData() { // Asynchronously call category API using namespace SpkUtils; VerifySingleRequest(mCategoryGetReply); mCategoryGetReply = STORE->SendApiRequest("type/get_type_list"); DeleteReplyLater(mCategoryGetReply); connect(mCategoryGetReply, &QNetworkReply::finished, this, &SpkMainWindow::CategoryDataReceived); } void SpkMainWindow::CategoryDataReceived() { QJsonValue retval; if(!SpkUtils::VerifyReplyJson(mCategoryGetReply, retval) || !retval.isArray()) { sErr(tr("Failed to load categories!")); // TODO: Switch to an error page return; } PopulateCategories(retval.toArray()); } void SpkMainWindow::EnterCategoryList(int aCategoryId, int aPage) { // Asynchronously call category API using namespace SpkUtils; VerifySingleRequest(mCategoryAppListGetReply); QJsonObject reqData; QJsonDocument reqDoc; reqData.insert("type_id", QJsonValue(aCategoryId)); reqData.insert("page", QJsonValue(aPage)); reqDoc.setObject(reqData); mCategoryAppListGetReply = STORE->SendApiRequest("application/get_application_list", reqDoc); DeleteReplyLater(mCategoryAppListGetReply); connect(mCategoryAppListGetReply, &QNetworkReply::finished, this, &SpkMainWindow::CategoryListDataReceived); setCursor(Qt::BusyCursor); ui->PageAppList->SetCurrentCategory(aCategoryId); // AppList needs to remember current category } void SpkMainWindow::CategoryListDataReceived() { QJsonValue retval; if(!SpkUtils::VerifyReplyJson(mCategoryAppListGetReply, retval) || !retval.isObject()) { sErrPop(tr("Failed to load app list of category! Type of retval: %1.").arg(retval.type())); return; } SwitchToPage(SpkUi::PgAppList); PopulateAppList(retval.toObject(), ""); setCursor(Qt::ArrowCursor); } void SpkMainWindow::SearchKeyword(QString aKeyword, int aPage) { using namespace SpkUtils; VerifySingleRequest(mCategoryAppListGetReply); QJsonObject reqData; QJsonDocument reqDoc; reqData.insert("application_name", QJsonValue(aKeyword)); reqData.insert("page", QJsonValue(aPage)); reqDoc.setObject(reqData); mCategoryAppListGetReply = STORE->SendApiRequest("application/get_application_list", reqDoc); mCategoryAppListGetReply->setProperty("keyword", aKeyword); DeleteReplyLater(mCategoryAppListGetReply); connect(mCategoryAppListGetReply, &QNetworkReply::finished, this, &SpkMainWindow::SearchDataReceived); setCursor(Qt::BusyCursor); } void SpkMainWindow::SearchDataReceived() { QJsonValue retval; if(!SpkUtils::VerifyReplyJson(mCategoryAppListGetReply, retval) || !retval.isObject()) { sErrPop(tr("Failed to search keyword! Type of retval: %1.").arg(retval.type())); return; } SwitchToPage(SpkUi::PgAppList); PopulateAppList(retval.toObject(), mCategoryAppListGetReply->property("keyword").toString()); setCursor(Qt::ArrowCursor); } void SpkMainWindow::PopulateAppList(QJsonObject appData, QString &&keyword) { auto w = ui->PageAppList; w->ClearAll(); static auto err = [](){ sErr("Received invalid application list data!"); SpkUiMessage::SendStoreNotification(tr("Received an invalid response. Please try again!")); return; }; int pgCurrent, pgTotal, totalApps; if(appData.contains("currentPage") && appData.value("currentPage").isDouble()) pgCurrent = appData.value("currentPage").toInt(); else return err(); if(appData.contains("totalPages") && appData.value("totalPages").isDouble()) pgTotal = appData.value("totalPages").toInt(); else return err(); if(appData.contains("count") && appData.value("count").isDouble()) totalApps = appData.value("count").toInt(); else return err(); w->SetPageStatus(pgTotal, pgCurrent, totalApps, keyword); if(!appData.contains("data") || !appData.value("data").isArray()) return err(); auto apps = appData.value("data").toArray(); for(auto &&i : apps) { if(i.isObject()) { auto j = i.toObject(); QString pkgName, displayName, description, iconPath; int appid; if(j.contains("package") && j.value("package").isString()) pkgName = j.value("package").toString(); else continue; if(j.contains("application_name_zh") && j.value("application_name_zh").isString()) displayName = j.value("application_name_zh").toString(); else continue; if(j.contains("description") && j.value("description").isString()) description = j.value("description").toString(); else continue; if(j.contains("application_id") && j.value("application_id").isDouble()) appid = j.value("application_id").toInt(); else continue; if(j.contains("icons") && j.value("icons").isString()) iconPath = j.value("icons").toString(); else continue; w->AddApplicationEntry(displayName, pkgName, description, iconPath, appid); } } } void SpkMainWindow::EnterAppDetails(int aAppId) { using namespace SpkUtils; VerifySingleRequest(mAppDetailsGetReply); QJsonObject reqData; QJsonDocument reqDoc; reqData.insert("application_id", QJsonValue(aAppId)); reqDoc.setObject(reqData); mAppDetailsGetReply = STORE->SendApiRequest("application/get_application_detail", reqDoc); DeleteReplyLater(mAppDetailsGetReply); connect(mAppDetailsGetReply, &QNetworkReply::finished, this, &SpkMainWindow::AppDetailsDataReceived); setCursor(Qt::BusyCursor); } void SpkMainWindow::AppDetailsDataReceived() { QJsonValue retval; if(!SpkUtils::VerifyReplyJson(mAppDetailsGetReply, retval) || !retval.isObject()) { sErrPop(tr("Failed to open app details page! Type of retval: %1.").arg(retval.type())); return; } SwitchToPage(SpkUi::PgAppList); PopulateAppDetails(retval.toObject()); setCursor(Qt::ArrowCursor); } void SpkMainWindow::PopulateAppDetails(QJsonObject appDetails) { QString pkgName, author, contributor, site, iconPath, arch, version, details, shortDesc, name; QStringList screenshots, tags; int packageSize; static auto err = [](){ sErr("Received invalid application details!"); SpkUiMessage::SendStoreNotification(tr("Received an invalid response. Please try again!")); return; }; if(appDetails.contains("package") && appDetails.value("package").isString()) pkgName = appDetails.value("package").toString(); else return err(); if(appDetails.contains("application_name_zh") && appDetails.value("application_name_zh").isString()) name = appDetails.value("application_name_zh").toString(); else name = pkgName; if(appDetails.contains("version") && appDetails.value("version").isString()) version = appDetails.value("version").toString(); else return err(); if(appDetails.contains("icons") && appDetails.value("icons").isString()) iconPath= appDetails.value("icons").toString(); if(appDetails.contains("author") && appDetails.value("author").isString()) author = appDetails.value("author").toString(); if(appDetails.contains("contributor") && appDetails.value("contributor").isString()) contributor = appDetails.value("contributor").toString(); if(appDetails.contains("website") && appDetails.value("website").isString()) site = appDetails.value("website").toString(); if(appDetails.contains("description") && appDetails.value("description").isString()) shortDesc = appDetails.value("description").toString(); if(appDetails.contains("more") && appDetails.value("more").isString()) details = appDetails.value("more").toString(); if(appDetails.contains("arch") && appDetails.value("arch").isString()) arch = appDetails.value("arch").toString(); if(appDetails.contains("size") && appDetails.value("size").isDouble()) packageSize = appDetails.value("size").toInt(); QJsonArray imgs; if(appDetails.contains("img_urls") && appDetails.value("img_urls").isArray()) imgs = appDetails.value("img_urls").toArray(); if(!imgs.isEmpty()) for(auto &&i : imgs) if(i.isString()) screenshots << i.toString(); QJsonArray tags_j; if(appDetails.contains("tags") && appDetails.value("tags").isArray()) imgs = appDetails.value("tags").toArray(); if(!tags_j.isEmpty()) for(auto &&i : tags_j) if(i.isString()) tags << i.toString(); // Details string has a strangely appended LF. IDK but still should remove it. shortDesc = shortDesc.trimmed(); auto w = ui->PageAppDetails; w->mPkgName->setText(pkgName); w->mAppTitle->setText(name); w->mAppShortDesc->setText(shortDesc); w->mAppDescription->setText(details); w->mAuthor->SetValue(author); w->mContributor->SetValue(contributor); // w->mSite->SetValue(site); // Doesn't look good, I disabled it temporarily. Better solution? w->SetWebsiteLink(site); w->mArch->SetValue(arch); w->mSize->SetValue(SpkUtils::BytesToSize(packageSize)); w->mVersion->setText(version); SwitchToPage(SpkUi::PgAppDetails); ui->AppDetailsItem->setHidden(false); ui->CategoryWidget->setCurrentItem(ui->AppDetailsItem); w->LoadAppResources(pkgName, iconPath, screenshots, tags); } void SpkMainWindow::ReloadThemedUiIcons() { for(auto &i : mThemedUiIconReferences) i.first->SetIcon(SpkUi::GetThemedIcon(i.second), QSize { 20, 20 }); } // ==================== Main Window Initialization ==================== void SpkMainWindow::Initialize() { connect(ui->SidebarMgr, &SpkUi::SpkSidebarSelector::SwitchToPage, this, &SpkMainWindow::SwitchToPage); connect(ui->SidebarMgr, &SpkUi::SpkSidebarSelector::SwitchToCategory, this, &SpkMainWindow::EnterCategoryList); connect(ui->PageAppList, &SpkUi::SpkPageAppList::SwitchListPage, this, &SpkMainWindow::EnterCategoryList); connect(ui->PageAppList, &SpkUi::SpkPageAppList::SwitchSearchPage, this, &SpkMainWindow::SearchKeyword); connect(ui->SearchEdit, &QLineEdit::returnPressed, [=](){ emit SearchKeyword(ui->SearchEdit->text(), 1); }); connect(ui->PageAppList, &SpkUi::SpkPageAppList::ApplicationClicked, this, &SpkMainWindow::EnterAppDetails); connect(ui->BtnDayNight, &QPushButton::clicked, this, &SpkMainWindow::SwitchDayNightTheme); if(SpkUi::States::IsUsingDtkPlugin) { connect(SpkUi::DtkPlugin, &SpkDtkPlugin::DarkLightThemeChanged, this, &SpkMainWindow::ReloadThemedUiIcons); } // Register themed button icons // mThemedUiIconReferences.append({ ui->BtnSettings, "settings" }); mThemedUiIconReferences.append({ ui->BtnDayNight, "daynight" }); ReloadThemedUiIcons(); } // ==================== Main Widget Initialization ==================== SpkUi::SpkMainWidget::SpkMainWidget(QWidget *parent) : QFrame(parent) { setObjectName("spk_mainwidget"); Pager = new QStackedWidget(this); Pager->setObjectName("spk_mw_pager"); Pager->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); TitleBar = new SpkTitleBar(this); TitleBar->setObjectName("spk_mw_titlebar"); TitleBar->SetUseIcon(false); TitleBar->SetTitle(""); VLayMain = new QVBoxLayout; VLayMain->setObjectName("spk_mw_main_vlay"); VLayMain->setSpacing(0); VLayMain->setContentsMargins(0, 0, 0, 0); VLayMain->addWidget(TitleBar); VLayMain->addWidget(Pager); VLaySidebar = new QVBoxLayout; VLaySidebar->setObjectName("spk_mw_sidebar_lay"); VLaySidebar->setSpacing(0); VLaySidebar->setContentsMargins(0, 0, 0, 0); SideBarRestrictor = new QWidget(this); SideBarRestrictor->setObjectName("spk_mw_sidebar_restrictor"); SideBarRestrictor->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); SideBarRestrictor->setMaximumWidth(200); SideBarRestrictor->setMinimumWidth(200); SideBarRestrictor->setLayout(VLaySidebar); HLaySideTop = new QHBoxLayout; HLaySideTop->setObjectName("spk_mw_sidebar_top_lay"); HLaySideTop->setSpacing(8); HLaySideTop->setContentsMargins(8, 4, 4, 4); StoreIcon = new QLabel(this); StoreIcon->setObjectName("spk_mw_icon"); StoreIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); StoreIcon->setMaximumSize({ 40, 40 }); StoreIcon->setMinimumSize({ 40, 40 }); StoreIcon->setPixmap(QIcon(":/icons/spark-store.svg").pixmap(StoreIcon->size())); SidebarMgr = new SpkSidebarSelector(this); SidebarMgr->setObjectName("spk_mw_sidebar_mgr"); BtnSettings = new SpkIconButton(this); BtnSettings->setObjectName("styPlainChkBtn"); BtnSettings->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); BtnSettings->setCheckable(true); BtnSettings->setFixedSize({ 40, 40 }); BtnSettings->SetIcon(QIcon(":/icons/settings.svg"), QSize(20, 20)); BtnSettings->setProperty("spk_pageno", 0); SidebarMgr->BindPageSwitcherButton(BtnSettings); BtnDayNight = new SpkIconButton(this); BtnDayNight->setObjectName("styPlainChkBtn"); BtnDayNight->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); BtnDayNight->setFixedSize({ 40, 40 }); BtnDayNight->SetIcon(QIcon(":/icons/daynight.svg"), QSize(20, 20)); HLaySideTop->addWidget(StoreIcon); HLaySideTop->addStretch(); HLaySideTop->addWidget(BtnDayNight); HLaySideTop->addWidget(BtnSettings); VLaySidebar->addLayout(HLaySideTop); using SpkUi::SpkSidebarSelector; CategoryWidget = new SpkSidebarTree(this); CategoryWidget->setObjectName("styMwCateg"); CategoryWidget->setAutoFillBackground(true); CategoryWidget->setColumnCount(1); CategoryWidget->setHeaderHidden(true); CategoryWidget->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); //============ Sidebar entries BEGIN ============ AppDetailsItem = new QTreeWidgetItem(QStringList(tr("App Details"))); AppDetailsItem->setData(0, SpkSidebarSelector::RoleItemIsCategory, false); AppDetailsItem->setData(0, SpkSidebarSelector::RoleItemCategoryPageId, SpkStackedPages::PgAppDetails); CategoryParentItem = new QTreeWidgetItem(QStringList(tr("Categories"))); CategoryParentItem->setFlags(CategoryParentItem->flags().setFlag(Qt::ItemIsSelectable, false)); #ifndef NDEBUG UiTestItem = new QTreeWidgetItem(QStringList(tr("UI TEST"))); UiTestItem->setData(0, SpkSidebarSelector::RoleItemIsCategory, false); UiTestItem->setData(0, SpkSidebarSelector::RoleItemCategoryPageId, SpkStackedPages::PgQssTest); #endif //============ Sidebar entries END ============ SidebarMgr->AddUnusableItem(CategoryParentItem); CategoryWidget->addTopLevelItem(AppDetailsItem); CategoryWidget->addTopLevelItem(CategoryParentItem); CategoryWidget->addTopLevelItem(UiTestItem); // Must be done after added into a view. AppDetailsItem->setHidden(true); // Hide until we actually open up a Details page CategoryParentItem->setExpanded(true); // FIXMEIFPOSSIBLE: Fusion adds extra gradient. // Details: https://forum.qt.io/topic/128190/fusion-style-kept-adding-an-extra- // layer-of-gradient-to-my-selected-item-of-qtreewidget-even-with-qss if(SpkUi::OldSystemStyle) CategoryWidget->setStyle(SpkUi::OldSystemStyle); VLaySidebar->addWidget(CategoryWidget); SidebarMgr->BindCategoryWidget(CategoryWidget); HorizontalDivide = new QHBoxLayout; HorizontalDivide->setObjectName("spk_mw_divide_hlay"); HorizontalDivide->setSpacing(0); HorizontalDivide->setContentsMargins(0, 0, 0, 0); HorizontalDivide->setAlignment(Qt::AlignLeft); if(!SpkUi::States::IsUsingDtkPlugin) HorizontalDivide->addSpacing(SpkWindow::BorderWidth); HorizontalDivide->addWidget(SideBarRestrictor); HorizontalDivide->addLayout(VLayMain); //============ Search Bar ============ SearchEdit = new SpkFocusLineEdit(this); SearchEdit->setPlaceholderText(tr("Press Enter to search")); SearchEdit->setFixedWidth(30); SearchEdit->setFixedHeight(30); SearchBarAnim = new QTimeLine(300, this); SearchBarAnim->setDuration(300); SearchBarAnim->setEasingCurve(QEasingCurve::OutExpo); SearchBarAnim->setUpdateInterval(20); ActClearSearchBar = SearchEdit->addAction(QIcon(":/icons/clear-input.svg"), QLineEdit::TrailingPosition); ActClearSearchBar->setVisible(false); // Invisible by default ActSearchIcon = SearchEdit->addAction(QIcon(":/icons/search-mini.svg"), QLineEdit::LeadingPosition); connect(SearchEdit, &SpkFocusLineEdit::focusGained, [=](){ ActClearSearchBar->setVisible(true); SearchBarAnim->setDirection(QTimeLine::Forward); SearchBarAnim->start(); }); connect(SearchEdit, &SpkFocusLineEdit::focusLost, [=](){ ActClearSearchBar->setVisible(false); SearchBarAnim->setDirection(QTimeLine::Backward); SearchBarAnim->start(); }); connect(SearchBarAnim, &QTimeLine::valueChanged, [=](qreal v){ SearchEdit->setFixedWidth(static_cast(250 * v) + 30); }); connect(ActClearSearchBar, &QAction::triggered, [=](){ SearchEdit->clear(); }); auto space = TitleBar->GetUserSpace(); space->addWidget(SearchEdit); space->addStretch(); //============ Pages ============= // Red-Black tree based map will be able to sort things. Just for convenience of ordering pages. QMap sorter; // Initialize pages PageAppList = new SpkUi::SpkPageAppList(this); PageAppList->setProperty("spk_pageid", SpkStackedPages::PgAppList); sorter[PgAppList] = PageAppList; PageAppDetails = new SpkUi::SpkPageAppDetails(this); PageAppDetails->setProperty("spk_pageid", SpkStackedPages::PgAppDetails); sorter[PgAppDetails] = PageAppDetails; #ifndef NDEBUG // If only in debug mode should we initialize QSS test page PageQssTest = new SpkUi::SpkPageUiTest(this); PageQssTest->setProperty("spk_pageid", SpkStackedPages::PgQssTest); sorter[PgQssTest] = PageQssTest; #endif for(auto i : sorter) Pager->addWidget(i); setLayout(HorizontalDivide); }