mirror of
https://gitee.com/spark-store-project/spark-store
synced 2025-07-23 05:52:22 +08:00
支持图标跟随主题,改进详情页,加入更好的多线程下载
新的下载会重试一个线程上的错误,一个线程崩溃次数过多会转移到队列里等待重新安排,其他的暂时没写
This commit is contained in:
parent
acf013d8ab
commit
6491b19f6e
@ -81,6 +81,7 @@ set(SOURCE_FILES
|
||||
inc/spkuimsg.h src/spkuimsg.cpp
|
||||
inc/spklogging.h src/spklogging.cpp
|
||||
inc/spkresource.h src/spkresource.cpp
|
||||
inc/spkdownload.h src/spkdownload.cpp
|
||||
)
|
||||
|
||||
include(cmake/FindLibNotify.cmake)
|
||||
|
@ -53,6 +53,11 @@ namespace SpkUi
|
||||
// TODO: tags
|
||||
}
|
||||
|
||||
void SpkPageAppDetails::SetWebsiteLink(QString url)
|
||||
{
|
||||
mWebsite->setText(QString("<a href=\"%1\">%1</a>").arg(url));
|
||||
}
|
||||
|
||||
SpkPageAppDetails::SpkPageAppDetails(QWidget *parent) : SpkPageBase(parent)
|
||||
{
|
||||
mMainArea = new QScrollArea;
|
||||
@ -81,6 +86,7 @@ namespace SpkUi
|
||||
mAppShortDesc->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
|
||||
mAppShortDesc->setMinimumWidth(100);
|
||||
mVersion = new QLabel;
|
||||
mWebsite = new QLabel;
|
||||
mPkgName = new QLabel;
|
||||
mPkgName->setObjectName("styDetPkg");
|
||||
|
||||
@ -90,6 +96,7 @@ namespace SpkUi
|
||||
mTitleLay->addWidget(mVersion);
|
||||
mTitleLay->addWidget(mAppShortDesc);
|
||||
mTitleLay->addWidget(mPkgName);
|
||||
mTitleLay->addWidget(mWebsite);
|
||||
mTitleLay->setSpacing(0);
|
||||
|
||||
mIconTitleLay = new QHBoxLayout;
|
||||
@ -118,7 +125,7 @@ namespace SpkUi
|
||||
mDetailLay->addWidget(mContributor);
|
||||
mDetailLay->addWidget(mSize);
|
||||
mDetailLay->addWidget(mArch);
|
||||
mDetailLay->addWidget(mSite);
|
||||
// mDetailLay->addWidget(mSite);
|
||||
|
||||
// mDetailWidget = new QWidget;
|
||||
// mDetailWidget->setLayout(mDetailLay);
|
||||
@ -133,6 +140,9 @@ namespace SpkUi
|
||||
mWid4MainArea->setLayout(mMainLay);
|
||||
|
||||
mMainArea->setWidget(mWid4MainArea);
|
||||
|
||||
mWebsite->setTextFormat(Qt::RichText);
|
||||
mWebsite->setOpenExternalLinks(true);
|
||||
}
|
||||
|
||||
void SpkPageAppDetails::ResourceAcquisitionFinished(int id, ResourceResult result)
|
||||
@ -178,4 +188,4 @@ namespace SpkUi
|
||||
setMinimumWidth(300);
|
||||
setAutoFillBackground(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,15 @@ SpkMainWindow::SpkMainWindow(QWidget *parent) : SpkWindow(parent)
|
||||
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)
|
||||
@ -296,7 +305,8 @@ void SpkMainWindow::PopulateAppDetails(QJsonObject appDetails)
|
||||
w->mAppDescription->setText(details);
|
||||
w->mAuthor->SetValue(author);
|
||||
w->mContributor->SetValue(contributor);
|
||||
w->mSite->SetValue(site);
|
||||
// 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);
|
||||
@ -306,6 +316,12 @@ void SpkMainWindow::PopulateAppDetails(QJsonObject appDetails)
|
||||
w->LoadAppResources(pkgName, iconPath, screenshots, tags);
|
||||
}
|
||||
|
||||
void SpkMainWindow::ReloadThemedUiIcons()
|
||||
{
|
||||
for(auto &i : mThemedUiIconReferences)
|
||||
i.first->setIcon(SpkUi::GetThemedIcon(i.second));
|
||||
}
|
||||
|
||||
// ==================== Main Window Initialization ====================
|
||||
|
||||
void SpkMainWindow::Initialize()
|
||||
@ -322,6 +338,17 @@ void SpkMainWindow::Initialize()
|
||||
[=](){ emit SearchKeyword(ui->SearchEdit->text(), 1); });
|
||||
connect(ui->PageAppList, &SpkUi::SpkPageAppList::ApplicationClicked,
|
||||
this, &SpkMainWindow::EnterAppDetails);
|
||||
connect(ui->BtnDayNight, &QPushButton::pressed,
|
||||
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" });
|
||||
}
|
||||
|
||||
// ==================== Main Widget Initialization ====================
|
||||
@ -384,8 +411,17 @@ SpkUi::SpkMainWidget::SpkMainWidget(QWidget *parent) : QFrame(parent)
|
||||
BtnSettings->setProperty("spk_pageno", 0);
|
||||
SidebarMgr->BindPageSwitcherButton(BtnSettings);
|
||||
|
||||
BtnDayNight = new QPushButton(this);
|
||||
BtnDayNight->setObjectName("styPlainChkBtn");
|
||||
BtnDayNight->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
BtnDayNight->setMaximumSize({ 40, 40 });
|
||||
BtnDayNight->setMinimumSize({ 40, 40 });
|
||||
BtnDayNight->setIconSize(QSize(20, 20));
|
||||
BtnDayNight->setIcon(SpkUi::GetThemedIcon("daynight"));
|
||||
|
||||
HLaySideTop->addWidget(StoreIcon);
|
||||
HLaySideTop->addStretch();
|
||||
HLaySideTop->addWidget(BtnDayNight);
|
||||
HLaySideTop->addWidget(BtnSettings);
|
||||
VLaySidebar->addLayout(HLaySideTop);
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "spkmsgbox.h"
|
||||
#include "spkpopup.h"
|
||||
#include "spklogging.h"
|
||||
#include "spkstore.h"
|
||||
|
||||
namespace SpkUi
|
||||
{
|
||||
|
@ -20,6 +20,7 @@ namespace SpkUi
|
||||
SpkPageAppDetails(QWidget *parent = nullptr);
|
||||
|
||||
void LoadAppResources(QString pkgName, QString icon, QStringList screenshots, QStringList tags);
|
||||
void SetWebsiteLink(QString url);
|
||||
|
||||
private:
|
||||
|
||||
@ -30,14 +31,19 @@ namespace SpkUi
|
||||
public:
|
||||
static constexpr QSize IconSize { 144, 144 };
|
||||
|
||||
// Main Area
|
||||
QScrollArea *mMainArea;
|
||||
QWidget *mDetailWidget, *mIconTitleWidget, *mWid4MainArea;
|
||||
QLabel *mAppTitle, *mAppIcon, *mAppDescription, *mAppShortDesc, *mPkgName, *mVersion;
|
||||
QLabel *mAppTitle, *mAppIcon, *mAppDescription, *mAppShortDesc, *mPkgName, *mVersion,
|
||||
*mWebsite;
|
||||
SpkDetailEntry *mAuthor, *mContributor, *mSite, *mArch, *mSize;
|
||||
SpkStretchLayout *mDetailLay;
|
||||
QVBoxLayout *mMainLay, *mTitleLay, *mLay4MainArea;
|
||||
QHBoxLayout *mIconTitleLay;
|
||||
|
||||
// Bottom bar
|
||||
QWidget *mBottomBar;
|
||||
QPushButton *mBtnInstall, *mBtnDownload, *mBtnUninstall, *mBtnRequestUpdate, *mBtnReport;
|
||||
};
|
||||
|
||||
class SpkDetailEntry : public QWidget
|
||||
@ -52,4 +58,4 @@ namespace SpkUi
|
||||
QLabel mTitle, mField;
|
||||
QHBoxLayout mLay;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
102
inc/spkdownload.h
Normal file
102
inc/spkdownload.h
Normal file
@ -0,0 +1,102 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spkstore.h"
|
||||
|
||||
/**
|
||||
* @note SpkDownloadMgr does NOT do download scheduling and other things; it's only a multithreaded
|
||||
* downloader; it manages the threads that are downloading stuff from the Internet.
|
||||
*/
|
||||
|
||||
class SpkDownloadMgr : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
SpkDownloadMgr(QObject *parent = nullptr);
|
||||
~SpkDownloadMgr();
|
||||
|
||||
static constexpr int MaximumThreadRetryCount = 3;
|
||||
|
||||
enum TaskResult
|
||||
{
|
||||
Success = 0,
|
||||
FailCannotCreateFile, ///< Failed because destination file cannot be created
|
||||
FailNoVaibleServer, ///< Failed because no server provides file size or download stalled on
|
||||
///< all of them
|
||||
Fail
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief DownloadWorker is not a real worker but more of a worker-like structure. It holds
|
||||
* information about one download thread, such as offset, total bytes and received bytes.
|
||||
* Workers' scheduling is still done by SpkDownloadMgr synchronously, avoiding
|
||||
* unnecessary race conditions and data safety problems.
|
||||
* DownloadWorker is also used in mFailureRetryQueue to indicate the blocks that needed
|
||||
* to be retried on other servers.
|
||||
*/
|
||||
struct DownloadWorker
|
||||
{
|
||||
QNetworkReply *Reply; ///< Reply from the network
|
||||
qint64 BeginOffset; ///< Where should a worker start downloading
|
||||
qint64 BytesNeeded; ///< How many bytes a worker should fetch in total
|
||||
qint64 BytesRecvd; ///< How many bytes a worker has received till now
|
||||
};
|
||||
|
||||
struct RemoteFileInfo
|
||||
{
|
||||
qint64 Size = -1;
|
||||
bool SupportPartialDownload = false; ///< Whether this file can be downloaded multithreaded
|
||||
QByteArray Md5;
|
||||
};
|
||||
|
||||
void SetNewServers(QList<QString> servers);
|
||||
|
||||
/**
|
||||
* @note This function uses BLOCKING IO!
|
||||
*/
|
||||
static RemoteFileInfo GetRemoteFileInfo(QUrl url);
|
||||
|
||||
private:
|
||||
QList<QString> mServers; ///< Multithreaded download
|
||||
|
||||
QList<DownloadWorker> mScheduledWorkers;
|
||||
|
||||
// If one scheduled task fails a few times in a row, we must give it up on that server and put
|
||||
// its responsible block onto this queue so we can try downloading the block from other servers
|
||||
QQueue<DownloadWorker> mFailureRetryQueue;
|
||||
|
||||
QFile mDestFile;
|
||||
QString mDestFolder, mCurrentRemotePath;
|
||||
RemoteFileInfo mCurrentRemoteFileInfo;
|
||||
|
||||
int mCurrentDownloadId;
|
||||
int mActiveWorkerCount;
|
||||
|
||||
public slots:
|
||||
void SetDestinationFolder(QString path);
|
||||
|
||||
/**
|
||||
* @brief StartNewDownload try to start new download task.
|
||||
* @param path File path. Domain name excluded. No leading slashes.
|
||||
* @param downloadId Emitted with progress, finish and error so the UI know whose status it is.
|
||||
* @return true for success and false for failure.
|
||||
*/
|
||||
bool StartNewDownload(QString path, int downloadId);
|
||||
bool PauseCurrentDownload();
|
||||
bool CancelCurrentDownload();
|
||||
|
||||
private slots:
|
||||
void WorkerFinish();
|
||||
void WorkerDownloadProgress(); ///< Be connected to ***QNetworkReply::readyRead***
|
||||
|
||||
private:
|
||||
void LinkReplyWithMe(QNetworkReply*);
|
||||
void TryScheduleFailureRetries();
|
||||
void TryScheduleFailureRetries(int i); ///< Try schedule on a specific task slot.
|
||||
|
||||
signals:
|
||||
void DownloadProgressed(qint64 bytes, qint64 total);
|
||||
void DownloadStopped(TaskResult status, int id);
|
||||
|
||||
|
||||
};
|
@ -144,7 +144,7 @@ namespace SpkUi
|
||||
QVBoxLayout *VLaySidebar;
|
||||
QHBoxLayout *HLaySideTop;
|
||||
QLabel *StoreIcon;
|
||||
QPushButton *BtnSettings, *BtnFeedback, *BtnLogs;
|
||||
QPushButton *BtnSettings, *BtnFeedback, *BtnLogs, *BtnDayNight;
|
||||
SpkSidebarTree *CategoryWidget;
|
||||
QMap<int, QTreeWidgetItem> *CategoryItemMap;
|
||||
SpkSidebarSelector *SidebarMgr;
|
||||
@ -184,11 +184,15 @@ class SpkMainWindow : public SpkWindow
|
||||
mCategoryAppListGetReply,
|
||||
mAppDetailsGetReply;
|
||||
SpkUi::SpkStackedPages mCurrentPage = SpkUi::PgInvalid;
|
||||
QList<QPair<QPushButton*, QString>> mThemedUiIconReferences;
|
||||
|
||||
public slots:
|
||||
void ReloadThemedUiIcons();
|
||||
void RefreshCategoryData();
|
||||
|
||||
private slots:
|
||||
void SwitchDayNightTheme();
|
||||
|
||||
void SwitchToPage(SpkUi::SpkStackedPages page);
|
||||
|
||||
void CategoryDataReceived();
|
||||
|
@ -32,6 +32,9 @@ class SpkStore : public QObject
|
||||
QString GetApiRequestUrl() { return mApiRequestUrl; }
|
||||
QNetworkReply *SendApiRequest(QString path, QJsonDocument param = QJsonDocument());
|
||||
QNetworkReply *SendResourceRequest(QString path); ///< WARNING: Only intended for SpkResource!
|
||||
QNetworkReply *SendDownloadRequest(QUrl file, qint64 fromByte = -1, qint64 toByte = -1);
|
||||
|
||||
QNetworkReply *SendCustomHeadRequest(QNetworkRequest);
|
||||
|
||||
private:
|
||||
SpkLogger *mLogger;
|
||||
|
58
resource/icons/daynight-dark.svg
Normal file
58
resource/icons/daynight-dark.svg
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48mm"
|
||||
height="48mm"
|
||||
viewBox="0 0 48 48"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
sodipodi:docname="night.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
width="48mm"
|
||||
inkscape:zoom="3.0296134"
|
||||
inkscape:cx="89.94547"
|
||||
inkscape:cy="101.16802"
|
||||
inkscape:window-width="1852"
|
||||
inkscape:window-height="1021"
|
||||
inkscape:window-x="68"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true">
|
||||
<sodipodi:guide
|
||||
position="24.364586,23.954072"
|
||||
orientation="0,-1"
|
||||
id="guide1317" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="图层 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path846"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:12;stroke-opacity:0.48333;paint-order:markers stroke fill"
|
||||
d="M 20.08627,5.4455699 A 19.456798,19.456798 0 0 0 5.1481639,24.360177 19.456798,19.456798 0 0 0 24.604857,43.81687 19.456798,19.456798 0 0 0 42.756719,31.347865 17.271181,17.271181 0 0 1 32.437969,34.778662 17.271181,17.271181 0 0 1 15.166674,17.507366 17.271181,17.271181 0 0 1 20.08627,5.4455699 Z" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="图层 2" />
|
||||
</svg>
|
After (image error) Size: 2.0 KiB |
134
resource/icons/daynight.svg
Normal file
134
resource/icons/daynight.svg
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
sodipodi:docname="daylight.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="-56.375"
|
||||
inkscape:cy="30.875"
|
||||
inkscape:window-width="1852"
|
||||
inkscape:window-height="1021"
|
||||
inkscape:window-x="68"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:snap-global="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid9" />
|
||||
<sodipodi:guide
|
||||
position="6.35,6.35"
|
||||
orientation="1,0"
|
||||
id="guide11" />
|
||||
<sodipodi:guide
|
||||
position="6.35,6.35"
|
||||
orientation="0,1"
|
||||
id="guide13" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="图层 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<circle
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:17.3175;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="path847"
|
||||
cx="6.3499999"
|
||||
cy="6.3499999"
|
||||
r="3.0736487" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="6.0344238"
|
||||
y="-12.238637"
|
||||
ry="0.31557631"
|
||||
transform="rotate(90)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-3"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="-0.31557623"
|
||||
y="-14.868893"
|
||||
ry="0.31557631"
|
||||
transform="rotate(135)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-6"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="-6.665576"
|
||||
y="-12.238638"
|
||||
ry="0.31557631"
|
||||
transform="scale(-1)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-7"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="-9.2958326"
|
||||
y="-5.8886371"
|
||||
ry="0.31557631"
|
||||
transform="rotate(-135)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-5"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="-6.665576"
|
||||
y="0.46136269"
|
||||
ry="0.31557631"
|
||||
transform="rotate(-90)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-35"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="-0.31557468"
|
||||
y="3.0916188"
|
||||
ry="0.31557631"
|
||||
transform="rotate(-44.999991)" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-62"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="6.0344238"
|
||||
y="0.46136251"
|
||||
ry="0.31557631" />
|
||||
<rect
|
||||
style="fill:#434343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:7.96599;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:45.3543;paint-order:markers stroke fill"
|
||||
id="rect2203-9"
|
||||
width="0.63115263"
|
||||
height="2.252275"
|
||||
x="8.6646795"
|
||||
y="-5.8886418"
|
||||
ry="0.31557631"
|
||||
transform="rotate(45.000025)" />
|
||||
</g>
|
||||
</svg>
|
After (image error) Size: 4.7 KiB |
60
resource/icons/settings.svg
Normal file
60
resource/icons/settings.svg
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
sodipodi:docname="settings.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="14.285166"
|
||||
inkscape:cx="25.095963"
|
||||
inkscape:cy="22.505864"
|
||||
inkscape:window-width="1852"
|
||||
inkscape:window-height="1021"
|
||||
inkscape:window-x="68"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid9" />
|
||||
<sodipodi:guide
|
||||
position="6.35,6.35"
|
||||
orientation="1,0"
|
||||
id="guide11" />
|
||||
<sodipodi:guide
|
||||
position="6.35,6.35"
|
||||
orientation="0,1"
|
||||
id="guide13" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="图层 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path3842"
|
||||
style="fill:#434343;fill-rule:evenodd;stroke:none;stroke-width:0.343937;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 2.4098143,1.9035478 0.7541001,2.0141066 a 4.0110148,4.0110148 0 0 0 -0.5134382,0.888368 l -2.12081888,0.3532617 9.178e-5,2.3814089 2.120632,0.3533485 a 4.0110148,4.0110148 0 0 0 0.5133361,0.8880906 l -0.7537904,2.0140909 2.0620563,1.190336 1.366532,-1.660758 a 4.0110148,4.0110148 0 0 0 0.5114618,0.03528 4.0110148,4.0110148 0 0 0 0.5114047,-0.0351 L 8.2281517,11.986371 10.290186,10.796452 9.5360854,8.7823455 A 4.0110148,4.0110148 0 0 0 10.049524,7.8939774 L 12.170342,7.5407157 12.170251,5.1593068 10.051736,4.8065257 A 4.0110148,4.0110148 0 0 0 9.5372976,3.9154917 L 10.290073,1.9037771 8.2280168,0.71344079 6.8636532,2.3717535 A 4.0110148,4.0110148 0 0 0 6.350023,2.3389153 4.0110148,4.0110148 0 0 0 5.8386182,2.3740101 l -1.36677,-1.66038137 z m 3.014144,2.8425009 A 1.8520833,1.8520833 0 0 1 7.9539511,5.4239583 1.8520833,1.8520833 0 0 1 7.2760416,7.9539511 1.8520833,1.8520833 0 0 1 4.7460487,7.2760416 1.8520833,1.8520833 0 0 1 5.4239583,4.7460487 Z" />
|
||||
</g>
|
||||
</svg>
|
After (image error) Size: 2.7 KiB |
@ -12,5 +12,8 @@
|
||||
<file>icons/clear-input.svg</file>
|
||||
<file>icons/search-mini.svg</file>
|
||||
<file>icons/broken-icon.svg</file>
|
||||
<file>icons/settings.svg</file>
|
||||
<file>icons/daynight-dark.svg</file>
|
||||
<file>icons/daynight.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@ -1,7 +0,0 @@
|
||||
|
||||
#include "spkjsonapiconsumer.h"
|
||||
|
||||
void SpkJsonApiConsumer::RequestUrl(QUrl aUrl)
|
||||
{
|
||||
mNet.
|
||||
}
|
19
src/main.cpp
19
src/main.cpp
@ -4,6 +4,9 @@
|
||||
#include "spkui_general.h"
|
||||
#include "spkstore.h"
|
||||
|
||||
#include "spkdownload.h"
|
||||
#include "spkmsgbox.h"
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@ -13,5 +16,21 @@ int main(int argc, char *argv[])
|
||||
|
||||
SpkStore store(false, LogPath);
|
||||
|
||||
// SpkDownloadMgr dl;
|
||||
// dl.SetNewServers({
|
||||
// "https://d1.store.deepinos.org.cn/",
|
||||
// "https://d2.store.deepinos.org.cn/",
|
||||
// "https://d3.store.deepinos.org.cn/",
|
||||
// "https://d4.store.deepinos.org.cn/",
|
||||
// "https://d5.store.deepinos.org.cn/"
|
||||
// });
|
||||
// dl.SetDestinationFolder("/tmp/");
|
||||
// dl.StartNewDownload("store/office/cn.com.10jqka/cn.com.10jqka_1.6.1.2_amd64.deb", 0);
|
||||
// QObject::connect(&dl, &SpkDownloadMgr::DownloadStopped,
|
||||
// [&](SpkDownloadMgr::TaskResult, int)
|
||||
// {
|
||||
// SpkMsgBox::StaticExec("Finished", "");
|
||||
// });
|
||||
|
||||
return QApplication::exec();
|
||||
}
|
||||
|
255
src/spkdownload.cpp
Normal file
255
src/spkdownload.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
|
||||
#include "spkdownload.h"
|
||||
#include "spkutils.h"
|
||||
#include <QEventLoop>
|
||||
|
||||
SpkDownloadMgr::SpkDownloadMgr(QObject *parent)
|
||||
{
|
||||
mDestFolder = CFG->value("download/dir", QDir::homePath() + "/.local/spark-store/downloads")
|
||||
.toString();
|
||||
|
||||
QDir dest(mDestFolder);
|
||||
if(!dest.exists())
|
||||
QDir().mkdir(mDestFolder);
|
||||
|
||||
// Distribution servers
|
||||
QString srvPaths = CFG->value("download/servers", "https://d.store.deepinos.org/").toString();
|
||||
mServers = srvPaths.split(";;");
|
||||
|
||||
mCurrentDownloadId = -1;
|
||||
mActiveWorkerCount = 0;
|
||||
}
|
||||
|
||||
SpkDownloadMgr::~SpkDownloadMgr()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::SetNewServers(QList<QString> servers)
|
||||
{
|
||||
mServers = servers;
|
||||
}
|
||||
|
||||
SpkDownloadMgr::RemoteFileInfo SpkDownloadMgr::GetRemoteFileInfo(QUrl url)
|
||||
{
|
||||
QEventLoop event;
|
||||
QNetworkRequest request;
|
||||
request.setUrl(QUrl(url));
|
||||
|
||||
// Use a HEAD request to get file's actual size.
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = STORE->SendCustomHeadRequest(request);
|
||||
connect(reply, &QNetworkReply::finished, &event, &QEventLoop::quit);
|
||||
event.exec();
|
||||
|
||||
RemoteFileInfo ret;
|
||||
if(reply->hasRawHeader("Content-Length"))
|
||||
ret.Size = reply->header(QNetworkRequest::ContentLengthHeader).toUInt();
|
||||
ret.SupportPartialDownload = (reply->rawHeader("Accept-Ranges") == QByteArrayLiteral("bytes"));
|
||||
|
||||
reply->deleteLater();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::SetDestinationFolder(QString path)
|
||||
{
|
||||
QDir dir(path);
|
||||
if(!dir.exists())
|
||||
{
|
||||
if(!QDir().mkdir(path))
|
||||
{
|
||||
sErrPop(tr("Cannot create download destination folder %1! It is now /tmp/.").arg(path));
|
||||
mDestFolder = "/tmp/";
|
||||
return;
|
||||
}
|
||||
}
|
||||
mDestFolder = path;
|
||||
}
|
||||
|
||||
bool SpkDownloadMgr::StartNewDownload(QString path, int downloadId)
|
||||
{
|
||||
if(mCurrentDownloadId != -1)
|
||||
return false; // Already downloading something
|
||||
|
||||
// Try get the file size first. If one server fails then go to next server
|
||||
RemoteFileInfo info;
|
||||
for(int i = 0; i < mServers.size() && info.Size == -1; i++)
|
||||
{
|
||||
info = GetRemoteFileInfo(mServers[i] + path);
|
||||
// TODO: Mark dead servers as unusable so they don't get scheduled first?
|
||||
}
|
||||
if(info.Size == -1) return false; // If all servers failed then we say it's a failure
|
||||
|
||||
mCurrentRemoteFileInfo = info;
|
||||
mActiveWorkerCount = 0;
|
||||
|
||||
// Create the destination file.
|
||||
mDestFile.close();
|
||||
mDestFile.setFileName(mDestFolder + '/' + SpkUtils::CutFileName(path));
|
||||
if(!mDestFile.open(QFile::ReadWrite))
|
||||
return false;
|
||||
|
||||
mCurrentRemotePath = path;
|
||||
|
||||
// Schedule tasks onto different servers if it's supported
|
||||
if(info.SupportPartialDownload && mServers.size() > 1)
|
||||
{
|
||||
int blockSize = info.Size / mServers.size();
|
||||
int i;
|
||||
for(i = 0; i < mServers.size() - 1; i++)
|
||||
{
|
||||
DownloadWorker worker
|
||||
{
|
||||
.BeginOffset = i * blockSize,
|
||||
.BytesNeeded = blockSize,
|
||||
.BytesRecvd = 0
|
||||
};
|
||||
worker.Reply =
|
||||
STORE->SendDownloadRequest(mServers[i] + path,
|
||||
worker.BeginOffset,
|
||||
worker.BeginOffset + worker.BytesNeeded);
|
||||
worker.Reply->setProperty("workerId", i);
|
||||
mScheduledWorkers.append(worker);
|
||||
}
|
||||
// Last one
|
||||
DownloadWorker worker
|
||||
{
|
||||
.BeginOffset = i * blockSize,
|
||||
.BytesNeeded = info.Size - i * blockSize,
|
||||
.BytesRecvd = 0
|
||||
};
|
||||
worker.Reply =
|
||||
STORE->SendDownloadRequest(mServers[i] + path,
|
||||
worker.BeginOffset,
|
||||
worker.BeginOffset + worker.BytesNeeded);
|
||||
worker.Reply->setProperty("workerId", i);
|
||||
mScheduledWorkers.append(worker);
|
||||
}
|
||||
else
|
||||
{
|
||||
DownloadWorker worker { .BeginOffset = 0, .BytesNeeded = info.Size, .BytesRecvd = 0 };
|
||||
worker.Reply = STORE->SendDownloadRequest(mServers[0] + path);
|
||||
mScheduledWorkers.append(worker);
|
||||
}
|
||||
|
||||
// Link the worker's replies with the manager
|
||||
for(auto &i : mScheduledWorkers)
|
||||
{
|
||||
LinkReplyWithMe(i.Reply);
|
||||
i.Reply->setProperty("failCount", 0); // Used for fail retry algorithm
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpkDownloadMgr::PauseCurrentDownload()
|
||||
{
|
||||
// UNIMPLEMENTED
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SpkDownloadMgr::CancelCurrentDownload()
|
||||
{
|
||||
// UNIMPLEMENTED
|
||||
return false;
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::WorkerFinish()
|
||||
{
|
||||
QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
|
||||
int id = reply->property("workerId").toInt();
|
||||
DownloadWorker &worker = mScheduledWorkers[id];
|
||||
|
||||
mActiveWorkerCount--;
|
||||
|
||||
if(reply->error() == QNetworkReply::NetworkError::NoError)
|
||||
{
|
||||
// Finished successfully, destroy associated stuff
|
||||
reply->deleteLater();
|
||||
worker.Reply = nullptr;
|
||||
|
||||
// Try schedule fail retries here.
|
||||
TryScheduleFailureRetries(id);
|
||||
|
||||
// Check if we're finished.
|
||||
if(mActiveWorkerCount == 0)
|
||||
{
|
||||
emit DownloadStopped(Success, mCurrentDownloadId);
|
||||
mScheduledWorkers.clear();
|
||||
mFailureRetryQueue.clear();
|
||||
mCurrentDownloadId = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Failed here! Update our offset and required bytes count etc.
|
||||
worker.BeginOffset += worker.BytesRecvd;
|
||||
worker.BytesNeeded -= worker.BytesRecvd;
|
||||
worker.BytesRecvd = 0;
|
||||
|
||||
if(reply->property("failCount").toInt() > MaximumThreadRetryCount)
|
||||
{
|
||||
// Failed too many times, this server is probably down or really bad condition.
|
||||
// Schedule it on other servers.
|
||||
reply->deleteLater();
|
||||
worker.Reply = nullptr;
|
||||
mFailureRetryQueue.enqueue(worker);
|
||||
return;
|
||||
}
|
||||
|
||||
// We can still retry.
|
||||
worker.Reply =
|
||||
STORE->SendDownloadRequest(mServers[id] + mCurrentRemotePath,
|
||||
worker.BeginOffset,
|
||||
worker.BeginOffset + worker.BytesNeeded);
|
||||
LinkReplyWithMe(worker.Reply);
|
||||
worker.Reply->setProperty("failCount", reply->property("failCount").toInt() + 1);
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::WorkerDownloadProgress()
|
||||
{
|
||||
QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
|
||||
DownloadWorker &worker = mScheduledWorkers[reply->property("workerId").toInt()];
|
||||
auto replyData = reply->readAll();
|
||||
|
||||
mDestFile.seek(worker.BeginOffset + worker.BytesRecvd);
|
||||
mDestFile.write(replyData);
|
||||
worker.BytesRecvd += replyData.size();
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::LinkReplyWithMe(QNetworkReply *reply)
|
||||
{
|
||||
mActiveWorkerCount++; // Each time you spin up a request you must do this so it's ok to do it here
|
||||
connect(reply, &QNetworkReply::readyRead, this, &SpkDownloadMgr::WorkerDownloadProgress);
|
||||
connect(reply, &QNetworkReply::finished, this, &SpkDownloadMgr::WorkerFinish);
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::TryScheduleFailureRetries()
|
||||
{
|
||||
if(mFailureRetryQueue.isEmpty())
|
||||
return;
|
||||
|
||||
for(int i = 0; i < mScheduledWorkers.size() && !mFailureRetryQueue.isEmpty(); i++)
|
||||
{
|
||||
TryScheduleFailureRetries(i);
|
||||
}
|
||||
}
|
||||
|
||||
void SpkDownloadMgr::TryScheduleFailureRetries(int i)
|
||||
{
|
||||
if(mFailureRetryQueue.isEmpty())
|
||||
return;
|
||||
if(mScheduledWorkers[i].Reply == nullptr)
|
||||
{
|
||||
// Schedule it here, it has finished its job
|
||||
mScheduledWorkers[i] = mFailureRetryQueue.dequeue();
|
||||
DownloadWorker &worker = mScheduledWorkers[i];
|
||||
mScheduledWorkers[i].Reply =
|
||||
STORE->SendDownloadRequest(mServers[i] + mCurrentRemotePath,
|
||||
worker.BeginOffset,
|
||||
worker.BeginOffset + worker.BytesNeeded);
|
||||
LinkReplyWithMe(mScheduledWorkers[i].Reply);
|
||||
}
|
||||
}
|
@ -176,6 +176,8 @@ void SpkResource::PurgeCachedResource(const QString &aPkgName, SpkResource::Reso
|
||||
{
|
||||
auto dir = QDir(mCacheDirectory + aPkgName + '/', ResourceName[aType] + '*');
|
||||
auto list = dir.entryList();
|
||||
sLog("Resource \"" + dir.absolutePath() + '/' + dir.nameFilters()[0] +
|
||||
"\" was requested to be removed.");
|
||||
|
||||
if(list.isEmpty())
|
||||
return;
|
||||
|
@ -59,7 +59,7 @@ SpkStore::SpkStore(bool aCli, QString &aLogPath)
|
||||
|
||||
// Initialize URL
|
||||
mApiRequestUrl = mCfg->value("url/api", "https://store.deepinos.org/api/").toString();
|
||||
mResourceRequestUrl = mCfg->value("url/res", "http://img.store.deepinos.org.cn/").toString();
|
||||
mResourceRequestUrl = mCfg->value("url/res", "http://d.deepinos.org.cn/").toString();
|
||||
|
||||
|
||||
mUserAgentStr = QString("Spark-Store/%1 Distro/%2")
|
||||
@ -89,6 +89,7 @@ QNetworkReply *SpkStore::SendApiRequest(QString aPath, QJsonDocument aParam)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setUrl(mApiRequestUrl + aPath);
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, mUserAgentStr);
|
||||
return mNetMgr->post(request, aParam.isEmpty() ? "{}" : aParam.toJson(QJsonDocument::Compact));
|
||||
@ -97,11 +98,31 @@ QNetworkReply *SpkStore::SendApiRequest(QString aPath, QJsonDocument aParam)
|
||||
QNetworkReply *SpkStore::SendResourceRequest(QString aPath)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
request.setUrl(mResourceRequestUrl + aPath);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, mUserAgentStr);
|
||||
return mNetMgr->get(request);
|
||||
}
|
||||
|
||||
QNetworkReply *SpkStore::SendDownloadRequest(QUrl file, qint64 fromByte, qint64 toByte)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setUrl(file);
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
if(fromByte != -1 && toByte != -1)
|
||||
{
|
||||
request.setRawHeader("Range", QString("bytes=%1-%2").arg(fromByte).arg(toByte).toLocal8Bit());
|
||||
}
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, mUserAgentStr);
|
||||
return mNetMgr->get(request);
|
||||
}
|
||||
|
||||
QNetworkReply *SpkStore::SendCustomHeadRequest(QNetworkRequest req)
|
||||
{
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, mUserAgentStr);
|
||||
return mNetMgr->head(req);
|
||||
}
|
||||
|
||||
static void InstallDefaultConfigs()
|
||||
{
|
||||
//TODO:STUB
|
||||
|
Loading…
x
Reference in New Issue
Block a user