diff --git a/.workflow/dtk-build-release-tag-20220425.yml b/.workflow/dtk-build-release-tag-20220425.yml index 14fc27a..e7631b1 100644 --- a/.workflow/dtk-build-release-tag-20220425.yml +++ b/.workflow/dtk-build-release-tag-20220425.yml @@ -2,7 +2,7 @@ version: '1.0' name: dtk-build-release-tag-20220425 displayName: dtk-build-release-tag triggers: - trigger: auto + trigger: manual push: tags: prefix: diff --git a/.workflow/pipeline-dtk-build-aarch64.yml b/.workflow/pipeline-dtk-build-aarch64.yml index 9e16cfa..77495d2 100644 --- a/.workflow/pipeline-dtk-build-aarch64.yml +++ b/.workflow/pipeline-dtk-build-aarch64.yml @@ -25,10 +25,12 @@ stages: - apt update - export DEBIAN_FRONTEND=noninteractive - echo "安装wget qemu-user-static" - - apt install wget qemu-user-static xz-utils -y + - apt install git wget qemu-user-static xz-utils binfmt-support -y - mkdir ../spark-store-git - mv * ../spark-store-git - - wget https://code.gitlink.org.cn/shenmo7192/debian-container-aarch64/raw/branch/master/DEBIANARM.tar.xz + - git clone https://gitlink.org.cn/shenmo7192/debian-container-aarch64.git + - mv debian-container-aarch64/DEBIANARM.tar.xz . + - rm -rf debian-container-aarch64 - tar -xf DEBIANARM.tar.xz - mkdir -p DEBIAN/root/build-spark - mv ../spark-store-git DEBIAN/root/build-spark/spark-store diff --git a/README.md b/README.md index 75dedee..1cf9391 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![star](https://gitee.com/deepin-community-store/spark-store/badge/star.svg?theme=gvp)](https://gitee.com/deepin-community-store/spark-store/stargazers) [![fork](https://gitee.com/deepin-community-store/spark-store/badge/fork.svg?theme=gvp)](https://gitee.com/deepin-community-store/spark-store/members) -## You are informed that the aarch64 support is EXPERIMENTAL and there is NO GUARANTEE that this branch will be supported in the future Spark Store aims to collect Linux apps for the convieniece of Linux new comers @@ -12,11 +11,11 @@ We set up this APP Store and collect APPs/tools that everyone need widely. Also All packages will be shared in our repository for users to get freely. -Distrobution supported:Deepin 20 ; Ubuntu 22.04 LTS / Ubuntu 20.04 LTS(May stop support in the future) ; UniontechOS Home 21 +Distrobution supported: -*About OpenKylin and deepin 23* +* amd64: deepin 20 / deepin 23 / Ubuntu 22.04 LTS / UniontechOS Home 21 +* arm64: UniontechOS Professional 1060 / Ubuntu 22.04 LTS / deepin 23 -The adaptation work is scheduled after their official release. You can track our Issue resoving progress here https://gitee.com/deepin-community-store/spark-store/board @@ -35,6 +34,7 @@ If you are using Debian11/Ubuntu 20.04, you will need extra dependency package. --- #### Compile and developement + **dev branch is for development, Flamescion branch is for amd64 and Reason branch is for arrch64.** Although, for the most of the time, Flamescion branch is same as Reason. For Deepin V20/UOS 21/ Debian 11 diff --git a/README.zh.md b/README.zh.md index 2f7ed02..58c13f5 100644 --- a/README.zh.md +++ b/README.zh.md @@ -2,20 +2,22 @@ [![star](https://gitee.com/deepin-community-store/spark-store/badge/star.svg?theme=gvp)](https://gitee.com/deepin-community-store/spark-store/stargazers) [![fork](https://gitee.com/deepin-community-store/spark-store/badge/fork.svg?theme=gvp)](https://gitee.com/deepin-community-store/spark-store/members) -## 请注意,aarch64的支持是实验性的,并未确认持续支持! -众所周知,国内的Linux应用比较少,wine应用难以获取,优质工具分散在民间各大论坛,无法形成合力,难以改善生态 +国内的Linux应用比较少,wine应用难以获取,优质工具分散在民间各大论坛,无法形成合力,难以改善生态 生态构建需要的不是某一方的单打独斗,而是人人行动起来,汇聚星火,产生燎原之势 我们创建了这个应用商店,广泛收录大家需要的软件包,搜集优质小工具,主动适配wine应用,存放到储存库供大家获取 -我们支持:Deepin 20 ; Ubuntu 22.04 LTS / Ubuntu 20.04 LTS(将会逐渐停止支持) ; UOS Home 21 + +发行版支持: + +* amd64: deepin 20 / deepin 23 / Ubuntu 22.04 / UOS家庭版20 +* arm64: UOS专业版1060 / Ubuntu 22.04 / deepin 23 + + **请注意:本程序不包含任何保证,若你要在UOS专业版上使用,请确保你打开了开发者模式且拥有排查错误的能力,风险自负!** ## 关于协作:分支相关的文档见 [这里](https://deepin-community-store.gitee.io/spark-wiki/#/Dev/Spark-Store-Git-Repo) -*关于OpenKylin和deepin 23* - -支持计划将会在对应系统发布正式版之后开始评估和执行 希望看到这里的人也可以加入我们的队伍,开发或者投递应用都很欢迎,共同构建Linux应用生态 @@ -33,6 +35,7 @@ --- #### 编译安装 + **dev分支用于开发,Flamescion分支是amd64,Reason分支是aarch64**。然而,绝大多数情况下,Flamescion和Reason的内容是一致的 Deepin V20/UOS 21 系统下, 安装依赖 diff --git a/debian/changelog b/debian/changelog index 5e98929..cb4d444 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +spark-store (4.2.7.3) stable; urgency=medium + + * 修复:aptss现在会正确地透传错误码而不是exit 0 + * 修复:下载时如果卡0%(无法下载metalink),会在超时后报错中断而不是一直傻等 + * 修复:排队下载时CPU占满单核的bug https://gitee.com/deepin-community-store/spark-store/issues/I7B91V + * 修复:在终端中打开的icon过大导致无法投稿到UOS + * 修复:v23下编译出错 + * 薪怎:支持崩溃日志收集系统 + + -- shenmo Sun, 5 Mar 2022 11:45:14 +0800 + + + spark-store (4.2.7.2) stable; urgency=medium * 新增:内置在终端打开功能 diff --git a/debian/control b/debian/control index cdc79d0..f84d01e 100644 --- a/debian/control +++ b/debian/control @@ -5,7 +5,8 @@ Priority: optional Build-Depends: debhelper (>= 9), pkg-config, - qtchooser (>= 55-gc9562a1-1~), + qtchooser (>= 55-gc9562a1-1~) | qt5-default, + qtbase5-dev, libqt5core5a, libqt5gui5, libqt5widgets5, diff --git a/pkg/usr/share/icons/hicolor/scalable/apps/open-me-in-terminal.png b/pkg/usr/share/icons/hicolor/scalable/apps/open-me-in-terminal.png index 993a76a..0ae8e54 100644 Binary files a/pkg/usr/share/icons/hicolor/scalable/apps/open-me-in-terminal.png and b/pkg/usr/share/icons/hicolor/scalable/apps/open-me-in-terminal.png differ diff --git a/src/application.cpp b/src/application.cpp index cede3ef..b3e5965 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -2,6 +2,8 @@ #include "mainwindow-dtk.h" #include "utils/utils.h" +#include +#include #include #include #include diff --git a/src/backend/downloadworker.cpp b/src/backend/downloadworker.cpp index 8b70540..d6f92af 100644 --- a/src/backend/downloadworker.cpp +++ b/src/backend/downloadworker.cpp @@ -5,13 +5,16 @@ #include #include +#define DEFAULTURL "d.store.deepinos.org.cn" +#define MAXWAITTIME 200000 + DownloadController::DownloadController(QObject *parent) { Q_UNUSED(parent) // 初始化默认域名 domains.clear(); - domains.append("d.store.deepinos.org.cn"); + domains.append(DEFAULTURL); /* domains = { @@ -37,7 +40,8 @@ bool checkMeatlink(QString metaUrl) { metaStatus.remove(); } - system("curl -I -s --connect-timeout 5 " + metaUrl.toUtf8() + " -w %{http_code} |tail -n1 > /tmp/spark-store/metaStatus.txt"); + QString cmd = QString("curl -I -s --connect-timeout 5 %1 -w %{http_code} |tail -n1 > /tmp/spark-store/metaStatus.txt").arg(metaUrl); + system(cmd.toUtf8().data()); if (metaStatus.open(QFile::ReadOnly) && QString(metaStatus.readAll()).toUtf8() == "200") { metaStatus.remove(); @@ -68,9 +72,13 @@ void gennerateDomain(QVector &domains) } if (domains.size() == 0) { - domains.append("d.store.deepinos.org.cn"); + domains.append(DEFAULTURL); } } + else + { + domains.append(DEFAULTURL); + } } /** @@ -102,34 +110,36 @@ void DownloadController::startDownload(const QString &url) // qDebug() << domains << domains.size(); } - QString aria2Command = "-d"; - QString aria2Urls = ""; - QString aria2Verbose = "--summary-interval=1"; - QString aria2SizePerThreads = "--min-split-size=1M"; - QString aria2NoConfig = "--no-conf"; - QString aria2NoSeeds = "--seed-time=0"; + QString aria2Command = "-d"; //下载目录 + QString aria2Urls = ""; //下载地址 + QString aria2Verbose = "--summary-interval=1"; //显示下载速度 + QString aria2SizePerThreads = "--min-split-size=1M"; //最小分片大小 + QString aria2NoConfig = "--no-conf"; //不使用配置文件 + QString aria2NoSeeds = "--seed-time=0"; //不做种 QStringList command; - QString downloadDir = "/tmp/spark-store/"; - QString aria2ConnectionPerServer = "--max-connection-per-server=1"; - QString aria2ConnectionMax = "--max-concurrent-downloads=16"; - QString aria2DNSCommand = "--async-dns-server=119.29.29.29,223.5.5.5"; + QString downloadDir = "/tmp/spark-store/"; //下载目录 + QString aria2ConnectionPerServer = "--max-connection-per-server=1"; //每个服务器最大连接数 + QString aria2ConnectionMax = "--max-concurrent-downloads=16"; //最大同时下载数 - if (useMetalink) + + if (useMetalink) //如果是metalink { command.append(metaUrl.toUtf8()); } else { - for (int i = 0; i < domains.size(); i++) + for (int i = 0; i < domains.size(); i++) //遍历域名 { command.append(replaceDomain(url, domains.at(i)).replace("+","%2B").toUtf8()); //对+进行转译,避免oss出错 } } - qint64 downloadSizeRecord = 0; - QString speedInfo = ""; - QString percentInfo = ""; + qint64 downloadSizeRecord = 0; //下载大小记录 + qint8 failDownloadTimes = 0; // 记录重试次数 + const qint8 maxRetryTimes = 3; //最大重试次数 + QString speedInfo = ""; //显示下载速度 + QString percentInfo = ""; //显示下载进度 command.append(aria2Command.toUtf8()); command.append(downloadDir.toUtf8()); command.append(aria2Verbose.toUtf8()); @@ -137,7 +147,7 @@ void DownloadController::startDownload(const QString &url) command.append(aria2SizePerThreads.toUtf8()); command.append(aria2ConnectionPerServer.toUtf8()); command.append(aria2ConnectionMax.toUtf8()); - command.append(aria2DNSCommand.toUtf8()); + if (useMetalink) { command.append(aria2NoSeeds.toUtf8()); @@ -152,11 +162,30 @@ void DownloadController::startDownload(const QString &url) cmd.start(); cmd.waitForStarted(-1); //等待启动完成 + // Timer + QTimer *timeoutTimer = new QTimer(this); + timeoutTimer->setSingleShot(true); // 单次触发 + connect(timeoutTimer, &QTimer::timeout, [&]() { + if (failDownloadTimes < maxRetryTimes) { + qDebug() << "Download timeout, restarting..."; + // 重新启动下载任务的代码 + restartDownload(cmd, command); // 调用重新启动下载任务的函数 + failDownloadTimes += 1; + timeoutTimer->start(MAXWAITTIME); // 重新启动定时器 + } else{ + emit errorOccur(tr("Download Failed, please retry :(")); // 下载失败 + downloadSuccess = false; + cmd.close(); + cmd.terminate(); // 终止当前的下载进程 + cmd.waitForFinished(); // 等待进程结束 + } + }); + connect(&cmd, &QProcess::readyReadStandardOutput, [&]() { + timeoutTimer->start(MAXWAITTIME); // 重置超时计时器,15秒超时 //通过读取输出计算下载速度 QString message = cmd.readAllStandardOutput().data(); - // qDebug() << message; message = message.replace(" ", ""); QStringList list; qint64 downloadSize = 0; @@ -179,10 +208,10 @@ void DownloadController::startDownload(const QString &url) speedInfo = message.mid(speedPlace1 + 3, speedPlace2 - speedPlace1 - 3); speedInfo += "/s"; } - // qDebug() << percentInfo << speedInfo; if (downloadSize >= downloadSizeRecord) { downloadSizeRecord = downloadSize; + timeoutTimer->stop(); // 如果有进度,停止超时计时器 } if (percentInfo == "OK") { @@ -244,6 +273,15 @@ void DownloadController::stopDownload() pidNumber = -1; } +void DownloadController::restartDownload(QProcess &cmd, const QStringList &command) +{ + cmd.terminate(); // 终止当前的下载进程 + cmd.waitForFinished(); // 等待进程结束 + cmd.setArguments(command); // 重新设置参数 + cmd.start(); // 重新启动下载 + cmd.waitForStarted(-1); // 等待启动完成 +} + qint64 DownloadController::getFileSize(const QString &url) { // 已经无需使用 qtnetwork 再获取 filesize,完全交给 aria2 来计算进度。 为保证兼容性,故保留此函数。 diff --git a/src/backend/downloadworker.h b/src/backend/downloadworker.h index 6b7110e..1f3217a 100644 --- a/src/backend/downloadworker.h +++ b/src/backend/downloadworker.h @@ -3,6 +3,7 @@ #include #include +#include class DownloadController : public QObject { @@ -14,6 +15,7 @@ public: void setFilename(QString filename); void startDownload(const QString &url); void stopDownload(); + void restartDownload(QProcess &cmd, const QStringList &command); qint64 getFileSize(const QString& url); QString replaceDomain(const QString& url, const QString domain); diff --git a/src/main.cpp b/src/main.cpp index e0f6fa1..541e3ab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,14 @@ #include "mainwindow-dtk.h" #include "utils/utils.h" +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -16,12 +24,97 @@ DCORE_USE_NAMESPACE DWIDGET_USE_NAMESPACE +static QString buildDateTime; + + +void crashHandler(int sig) { + void *array[50]; + size_t size = backtrace(array, 50); + if (size == 0) { + perror("backtrace"); + exit(1); + } + + time_t t = time(NULL); + struct tm tm = *localtime(&t); + char filename[128]; + snprintf(filename, sizeof(filename), "/tmp/spark_store_crash_log_%04d%02d%02d_%02d%02d%02d.txt", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + + std::ofstream logFile(filename, std::ios::out); + if (!logFile.is_open()) { + perror("ofstream"); + exit(1); + } + + logFile << "Please send this log to the developer. QQ Group: 872690351\n"; + logFile << "Build Date and Time: " << buildDateTime.toStdString() << "\n"; + + FILE *fp = popen("uname -m", "r"); + if (fp) { + char buffer[256]; + if (fgets(buffer, sizeof(buffer), fp) != NULL) { + // 移除换行符 + buffer[strcspn(buffer, "\n")] = 0; + logFile << "CPU Architecture: " << buffer << "\n"; + } + pclose(fp); + } else { + logFile << "Failed to gather CPU architecture info.\n"; + } + + + + + FILE *fp2 = popen("lsb_release -a", "r"); + if (fp2) { + char buffer[256]; + while (fgets(buffer, sizeof(buffer), fp2) != NULL) { + logFile << buffer; + } + pclose(fp2); + } else { + logFile << "Failed to gather distribution info.\n"; + } + + + logFile << "Error: signal " << sig << ":\n"; + for (size_t i = 0; i < size; i++) { + char **strings = backtrace_symbols(&array[i], 1); + if (strings != NULL && strings[0] != NULL) { + logFile << strings[0] << "\n"; + } else { + logFile << "Failed to get symbol.\n"; + } + free(strings); + } + + logFile.close(); + + char openCmd[256]; + snprintf(openCmd, sizeof(openCmd), "xdg-open %s", filename); + if (system(openCmd) == -1) { + perror("system"); + } + + fprintf(stderr, "Error: signal %d:\n", sig); + backtrace_symbols_fd(array, size, STDERR_FILENO); + + exit(1); +} + + int main(int argc, char *argv[]) { + // 崩溃处理 + signal(SIGSEGV, crashHandler); // 注册SIGSEGV处理函数 + + // Get build time static const QDate buildDate = QLocale(QLocale::English).toDate(QString(__DATE__).replace(" ", " 0"), "MMM dd yyyy"); static const QTime buildTime = QTime::fromString(__TIME__, "hh:mm:ss"); - static const QString buildDateTime = buildDate.toString("yyyy.MM.dd") + "-" + buildTime.toString("hh:mm:ss"); + buildDateTime = buildDate.toString("yyyy.MM.dd") + "-" + buildTime.toString("hh:mm:ss"); + // NOTE: 提前设置组织名称和应用名称,避免配置文件位置错误 DApplication::setOrganizationName("spark-union"); diff --git a/src/widgets/big_image.cpp b/src/widgets/big_image.cpp index 2549b59..95944e1 100644 --- a/src/widgets/big_image.cpp +++ b/src/widgets/big_image.cpp @@ -17,7 +17,8 @@ big_image::big_image(DBlurEffectWidget *parent) : DBlurEffectWidget(parent), layout->addWidget(m_image); layout->setMargin(0); - // m_image->setParent(this); + // Make sure the image has a parent so that it will be freed. + m_image->setParent(this); // m_image->setMaximumSize(1360,768); m_image->setAlignment(Qt::AlignCenter); } diff --git a/src/widgets/downloadlistwidget.cpp b/src/widgets/downloadlistwidget.cpp index 89fa8ba..f253fb6 100644 --- a/src/widgets/downloadlistwidget.cpp +++ b/src/widgets/downloadlistwidget.cpp @@ -169,6 +169,7 @@ void DownloadListWidget::httpFinished() // 完成下载 { while (downloaditemlist[nowDownload - 1]->readyInstall() == -1) // 安装当前应用,堵塞安装,后面的下载suspend { + QThread::msleep(500); // 休眠500ms,减少CPU负担 continue; } downloaditemlist[nowDownload - 1]->free = true; @@ -179,17 +180,15 @@ void DownloadListWidget::httpFinished() // 完成下载 // 如果有排队则下载下一个 qDebug() << "Download: 切换下一个下载..."; nowDownload += 1; - while (downloaditemlist[nowDownload - 1]->close) + while (nowDownload <= allDownload && downloaditemlist[nowDownload - 1]->close) { nowDownload += 1; - if (nowDownload >= allDownload) - { - nowDownload = allDownload; - return; - } } - QString fileName = downloaditemlist[nowDownload - 1]->getName(); - startRequest(urList.at(nowDownload - 1), fileName); + if (nowDownload <= allDownload) + { + QString fileName = downloaditemlist[nowDownload - 1]->getName(); + startRequest(urList.at(nowDownload - 1), fileName); + } } }); } @@ -236,19 +235,23 @@ void DownloadListWidget::on_pushButton_clicked() void DownloadListWidget::slotInstallFinished(bool success) { // NOTE: 仅在安装成功后判断是否需要退出后台 - if (success) { - toDownload -= 1; // 安装完以后减少待安装数目 - qDebug() << "Download: 还没有下载的数目:" << toDownload; + if (!success) { + qDebug() << "Download: install failed"; + return; + } - if (toDownload == 0) + if (toDownload > 0) { + toDownload -= 1; + qDebug() << "Download: toDownload" << toDownload; + } + + if (toDownload == 0) { + Application *app = qobject_cast(qApp); + MainWindow *mainWindow = app->mainWindow(); + if (mainWindow->isCloseWindowAnimation() == true) { - Application *app = qobject_cast(qApp); - MainWindow *mainWindow = app->mainWindow(); - if (mainWindow->isCloseWindowAnimation() == true) - { - qDebug() << "Download: 后台安装结束,退出程序"; - qApp->quit(); - } + qDebug() << "Download: 后台安装结束,退出程序"; + qApp->quit(); } } } diff --git a/tool/aptss b/tool/aptss index fce5398..88650e2 100755 --- a/tool/aptss +++ b/tool/aptss @@ -76,6 +76,7 @@ if [ "$1" = "install" ] || [ "$1" = "upgrade" ] || [ "$1" = "full-upgrade" ] ; ret="$?" if [ "$ret" -ne 0 ];then echo -e "\e[1;33m$TRANSHELL_CONTENT_PLEASE_USE_APTSS_INSTEAD_OF_APT\e[0m" +exit $ret fi diff --git a/translations/spark-store_en.ts b/translations/spark-store_en.ts index e13a3b6..d031691 100644 --- a/translations/spark-store_en.ts +++ b/translations/spark-store_en.ts @@ -268,16 +268,24 @@ DAboutDialog - + Version: %1 - + %1 is released under %2 + + DownloadController + + + Download Failed, please retry :( + + + DownloadItem @@ -506,19 +514,19 @@ QObject - - + + Spark Store - + <span style=' font-size:10pt;font-weight:60;'>An appstore powered by community</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>Spark developers</span> - + Spark Project diff --git a/translations/spark-store_fr.ts b/translations/spark-store_fr.ts index 50f58ff..5e574ff 100644 --- a/translations/spark-store_fr.ts +++ b/translations/spark-store_fr.ts @@ -268,16 +268,24 @@ DAboutDialog - + Version: %1 Version: %1 - + %1 is released under %2 %1 publié sous %2 + + DownloadController + + + Download Failed, please retry :( + + + DownloadItem @@ -506,19 +514,19 @@ QObject - - + + Spark Store Le Spark store - + <span style=' font-size:10pt;font-weight:60;'>An appstore powered by community</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>Spark developers</span> <span style=' font-size:10pt;font-weight:60;'>An appstore powered by community</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>Spark developers</span> - + Spark Project Le projet Spark diff --git a/translations/spark-store_zh_CN.ts b/translations/spark-store_zh_CN.ts index 48c412f..78c024e 100644 --- a/translations/spark-store_zh_CN.ts +++ b/translations/spark-store_zh_CN.ts @@ -268,16 +268,24 @@ DAboutDialog - + Version: %1 版本:%1 - + %1 is released under %2 %1遵循%2协议发布 + + DownloadController + + + Download Failed, please retry :( + + + DownloadItem @@ -506,19 +514,19 @@ QObject - - + + Spark Store 星火应用商店 - + <span style=' font-size:10pt;font-weight:60;'>An appstore powered by community</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>Spark developers</span> <span style=' font-size:10pt;font-weight:60;'>一款由社区提供的应用商店</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>星火计划开发者</span> - + Spark Project 星火计划 diff --git a/translations/spark-store_zh_TW.ts b/translations/spark-store_zh_TW.ts index 417b8e8..d4f2af1 100644 --- a/translations/spark-store_zh_TW.ts +++ b/translations/spark-store_zh_TW.ts @@ -268,16 +268,24 @@ DAboutDialog - + Version: %1 版本:%1 - + %1 is released under %2 %1遵循%2协议发布 + + DownloadController + + + Download Failed, please retry :( + + + DownloadItem @@ -506,19 +514,19 @@ QObject - - + + Spark Store 星火应用商店 - + <span style=' font-size:10pt;font-weight:60;'>An appstore powered by community</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>Spark developers</span> <span style=' font-size:10pt;font-weight:60;'>一款由社区提供的应用商店</span><br/><a href='https://www.spark-app.store/'>https://www.spark-app.store</a><br/><span style=' font-size:12pt;'>星火计划开发者</span> - + Spark Project 星火计划