Compare commits

...

282 Commits
1.0 ... non-dtk

Author SHA1 Message Date
RigoLigo
144103d8da 更新翻译流程 2022-10-02 21:18:32 +08:00
RigoLigo
ee876856c2 修复了主界面点链接后鼠标移到窗口操作区域后窗口位移的问题 2022-08-06 22:44:14 +08:00
RigoLigoRLC
3bd76b865d 实现垃圾文件计数和清理 2022-04-20 14:53:21 +08:00
RigoLigoRLC
8d40a0598e 新增截图预览 2022-03-13 20:27:03 +08:00
RigoLigoRLC
904cc3cb1c 修复在第一次下载进度报告前取消下载导致崩溃的问题 2022-02-21 18:01:26 +08:00
RigoLigoRLC
b0a2d64b71 添加翻译(终于) 2022-02-20 20:44:53 +08:00
RigoLigoRLC
393b3f73d5 修改日志头使文字对齐 2022-02-20 18:23:11 +08:00
RigoLigoRLC
7b2312f8ce 修正桌面提示消息中错误的应用程序名称 2022-02-20 18:22:43 +08:00
RigoLigoRLC
26a0c8133b 修正SpkAbout缺少的Q_OBJECT 2022-02-20 18:19:28 +08:00
RigoLigoRLC
aa58da05db 修复优麒麟上打不开首页的问题 2022-02-19 20:15:45 +08:00
RigoLigoRLC
3ecd3310ad 严格化进程错误退出判断条件 2022-02-19 20:10:41 +08:00
RigoLigoRLC
90ab4cb87f 修复一安装即完成的bug 2022-02-19 20:10:11 +08:00
RigoLigoRLC
d33e28b024 完善下载页面按钮的功能 2022-02-19 19:30:10 +08:00
RigoLigoRLC
8f32141726 修复SpkDownloadMgr重大bug
1. 原有依靠Qt Network的连接超时不可靠,改为自己使用Watchdog值跟踪。
2. 原有代码在请求时大量未对Reply设置正确的workerId,已修正
3. 原有代码不能自动在出错worker出错但其他worker已经全部完成时进行得分配,已修正
4. 原有代码ActiveWorkerCount计算方法不准确,每次link都会增加,但是由于重试重分配都会Link而且Worker不会从
List中删除,所以已改为任务开始时一次加满
5. 原有代码忘记在WorkerFinish时检查有否未写入的cache以及未关闭下载文件
6. 原有代码未将Reply的errorOccurred信号与WorkerError槽连接,导致这个槽并没有发挥什么卵用

测试文件改为有道词典,小一点
增加了等待HEAD请求时光标变成忙的特性
2022-02-19 02:38:03 +08:00
RigoLigoRLC
03f157f620 修复快速点击下载时产生的数据竞争
mNextDownloadId没来得及自增就被再次使用导致下载出现问题
2022-02-17 23:27:19 +08:00
RigoLigoRLC
8f8f479873 完善设置中的亮色暗色设置与UI的集成 2022-02-17 18:16:41 +08:00
RigoLigoRLC
2481adf29c 修复打开之后不单击树中任何页面就切换到设置时默认页面不能取消选择的bug 2022-02-16 01:18:21 +08:00
RigoLigoRLC
2bba8f57ba 将SpkTitleBar的底色改为默认纯色
有计划寻找一个合适的渐变色
2022-02-16 01:10:51 +08:00
RigoLigoRLC
c71582997b 添加界面组件间的分隔线方便识别
顺便增加了DVL(DivideLine)色
2022-02-16 00:57:55 +08:00
RigoLigoRLC
55795601e0 重构MainWindow的主界面
使用主窗口自带标题栏,摒弃侧栏和几个按钮独立的SideBarVLay,将之合并到原标题栏中
2022-02-16 00:28:58 +08:00
RigoLigoRLC
0a351ad40d 添加投稿按钮 2022-02-15 23:58:15 +08:00
RigoLigoRLC
ed233e2625 完善了APT包管理器后端的功能,基本实现安装 2022-02-15 23:36:04 +08:00
RigoLigoRLC
ae987f9542 实现分类加载失败可重新加载的UI 2022-02-13 23:13:41 +08:00
RigoLigoRLC
f655d0d94d 添加了Debian使用的几种包管理器 2022-02-13 22:45:06 +08:00
RigoLigoRLC
28e2c0d082 添加APT的安装菜单提示,修改安装方法使之返回Ignored 2022-02-13 21:24:49 +08:00
RigoLigoRLC
2221d5c816 加入包管理器模块雏形 2022-02-13 21:18:48 +08:00
RigoLigoRLC
f2e417e02a 更换SpkWindow基类为QWidget并修复多个问题
修复mResizable为false时阻止窗口移动的问题
更改About窗口为固定大小
2022-02-06 22:40:46 +08:00
RigoLigoRLC
58a0336a23 添加了首页的雏形,和一个关于如何添加页面的文档 2022-02-06 20:16:04 +08:00
RigoLigoRLC
7756bda006 删除误提交的测试UI主题色用代码 2022-02-05 18:21:30 +08:00
RigoLigoRLC
89e326e69e 为开发版客户端指定不一样的UA防止服务端数据干扰 2022-02-05 18:20:05 +08:00
RigoLigoRLC
0697acd9e4 修复Toast(SpkPopup)动画结束后不隐藏的bug 2022-02-05 00:39:01 +08:00
RigoLigoRLC
96ba6770b0 修复API call失败后鼠标不会回到默认状态的bug 2022-02-05 00:34:39 +08:00
RigoLigoRLC
908dd7d7a7 修复侧边栏上的按钮在选中时再单击会被取消选中的问题 2022-02-05 00:31:05 +08:00
RigoLigoRLC
af40213c5a 新增了配置的保存,以及第一次打开程序时安装默认配置 2022-02-04 23:27:37 +08:00
RigoLigoRLC
aa22cd7ff2 添加设置UI 2022-01-27 21:41:33 +08:00
RigoLigoRLC
02530de7da Temporary commit 2022-01-26 16:50:55 +08:00
RigoLigoRLC
974d0032e3 补齐缺少的文件 2021-12-17 13:02:33 +08:00
RigoLigoRLC
179e57b9b5 下载页面雏形,修复SpkPopup(改用弹出),添加应用列表固定图标缓存 2021-12-12 00:25:19 +08:00
RigoLigoRLC
f5a31affff 修复了一些不应有焦点控件按tab会获得焦点的问题 2021-12-11 23:45:09 +08:00
RigoLigoRLC
523640ba58 更正刚刚打开应用时的错误的亮暗色图标 2021-12-05 18:26:38 +08:00
RigoLigoRLC
cefd309097 新增SpkIconButton为主题和按钮工作减负 2021-12-05 18:09:31 +08:00
RigoLigoRLC
2e04d05b34 修复快速从应用列表进入应用详情时的崩溃
正确地断开过期的信号,正确地处理过期的请求
2021-12-02 18:20:28 +08:00
RigoLigoRLC
6491b19f6e 支持图标跟随主题,改进详情页,加入更好的多线程下载
新的下载会重试一个线程上的错误,一个线程崩溃次数过多会转移到队列里等待重新安排,其他的暂时没写
2021-11-28 02:11:54 +08:00
RigoLigoRLC
acf013d8ab 更改裂图的图标为自绘的图标 2021-09-19 21:23:34 +08:00
RigoLigoRLC
2b12c38f50 添加应用详情页面 2021-09-19 20:23:41 +08:00
RigoLigo
d2ab7cfbf7 Fix the excess margin of appitem on Deepin
(committed in VM)
2021-09-07 16:45:18 +08:00
RigoLigoRLC
c0789ca72f 补充忘记加入Git的SVG 2021-09-07 14:33:03 +08:00
RigoLigoRLC
90d44b85c1 微调QSS让暗色的应用列表不那么刺眼 2021-09-07 14:25:08 +08:00
RigoLigoRLC
91292f6550 微调QSS支持被关闭的按键 2021-09-07 00:09:54 +08:00
RigoLigoRLC
f96aa8150a 加入应用列表的搜索功能 2021-09-06 23:49:15 +08:00
RigoLigoRLC
f5649a121f 加入应用列表的翻页功能 2021-09-06 20:19:52 +08:00
RigoLigoRLC
4ede620e71 修复应用列表首次使用不会完全进行所有任务的问题 2021-09-03 16:02:38 +08:00
RigoLigoRLC
ca57a7ac86 实现应用列表和SpkResource
SpkResource现有已知问题:首次使用不会完全进行所有任务。
2021-09-03 00:48:24 +08:00
RigoLigoRLC
774e347957 让主题色略微更加饱满 2021-09-01 17:03:56 +08:00
RigoLigoRLC
764f6eee73 修正SpkUi初始化中的亮暗色更改判断逻辑错误 2021-09-01 16:43:09 +08:00
RigoLigoRLC
8ac76b668e 修正DtkPlugin中的亮暗色更改判断逻辑错误 2021-09-01 16:23:28 +08:00
RigoLigoRLC
5b946bb91a 删除SpkWindow::SetWindowStyle 2021-09-01 16:12:59 +08:00
RigoLigoRLC
6ddf44516a 对DDE下主题切换的修复 2021-09-01 16:09:49 +08:00
RigoLigoRLC
f50b014b77 删除不再有用的文件 2021-08-31 15:13:46 +08:00
RigoLigoRLC
5cbd758f67 增加亮色模式支持,小幅修改QSS和UI元素大小 2021-08-19 21:23:58 +08:00
RigoLigoRLC
04ebc6ed20 重写QSS组件,让主题和元素更改更方便 2021-08-19 02:00:16 +08:00
RigoLigoRLC
dd00465d83 删除不再有用的文件 2021-08-18 16:50:36 +08:00
RigoLigoRLC
f6baf41306 新增了SpkAppItem,更改style型对象名,修复侧边栏bug 2021-08-18 16:49:09 +08:00
RigoLigo
d49917c6fb 加入ColorSet的enum方便获取,SpkLoading使用主题色 2021-07-23 14:21:04 +08:00
RigoLigoRLC
e2821b13e0 去除代码提交日期信息 2021-07-22 19:57:26 +08:00
RigoLigoRLC
2b2bb06d02 新增用于About和其他详细信息的%14颜色 2021-07-22 19:56:18 +08:00
RigoLigoRLC
b3a9485bac UiTest新增About窗口测试 2021-07-22 19:53:07 +08:00
RigoLigoRLC
4f0990293a SpkPageQssTest重命名为SpkPageUiTest 2021-07-22 19:26:00 +08:00
RigoLigoRLC
3eb2a0d63a 单击版本号打开官网 2021-07-22 19:25:23 +08:00
RigoLigoRLC
8198dc48c5 加入SpkAbout(无内容) 2021-07-22 13:07:41 +08:00
RigoLigoRLC
f7becc1a66 修复安装默认配置时的bug 2021-07-21 02:31:47 +08:00
RigoLigoRLC
677cde3682 修复非DDE下移动窗口时鼠标不会在放开时还原的问题 2021-07-21 01:42:30 +08:00
RigoLigoRLC
ac9c725dfc 修复QSS在亮色下的显示和Tree的branch 2021-07-21 01:41:00 +08:00
RigoLigoRLC
270bf66b32 删除不慎加入的FFmpeg标头 2021-07-21 01:28:16 +08:00
RigoLigoRLC
99083d2bcb 添加商店内弹出窗消息
使用SpkUiMessage::SendStoreNotification激活,必须在SpkStore构造函数加载完全局SpkPopup类之后才可
使用。
2021-07-20 15:15:37 +08:00
RigoLigoRLC
e3c43198b9 添加API调用接口,添加读取分类,添加SpkUtils实用函数
使用了SpkSidebarTree子类实现对TreeWidget的特殊要求:不能取消选择,不能拖拽选择。

API调用接口暂时写死。API会获取

配置文件已添加但暂不使用。
2021-07-17 19:22:31 +08:00
RigoLigoRLC
48c9046993 修正非DDE下TreeWidget分支没有隐藏的问题 2021-07-08 12:30:21 +08:00
RigoLigo
09c3f8f67c 隐藏滚动条两端的细调整按钮 2021-07-08 00:58:58 +08:00
RigoLigo
d8b4a031e2 修复错误的QSS导致的滚动条不正确问题 2021-07-08 00:41:08 +08:00
RigoLigo
0a6e86dba7 使主题色依照DDE主题色改变
因为AUTOMOC不会在编译主程序的时候再对插件接口类进行MOC,所以需要在主程序内加一份一模一样的插件接口类定义。
2021-07-08 00:38:11 +08:00
RigoLigo
6536d3230f 更正SPARK_NO_DXCB环境变量的判断方式 2021-07-07 20:25:11 +08:00
RigoLigo
0885b7ca13 修复无DXCB且WM不支持拖动情况下窗口的拖拽 2021-07-07 18:23:38 +08:00
RigoLigoRLC
0729cfffb7 可执行文件名改为spark-store 2021-07-06 11:49:05 +08:00
RigoLigoRLC
fc4f2ddcc5 SpkMsgBox对话框弹出时固定大小 2021-07-06 11:13:13 +08:00
RigoLigo
4df8bf7d0a 进度更新 2021-07-05 23:35:50 +08:00
RigoLigo
8991043127 Fix library path issues 2021-07-02 21:17:45 +08:00
RigoLigo
9c080f8efb 进度更新 2021-07-02 20:12:09 +08:00
RigoLigo
8579e901d4 Lower CMake version requirement
To make CI running at least
2021-06-15 22:23:03 +08:00
RigoLigoRLC
4cf2f46866 Edited Jenkinsfile for CMake CI compile, untested 2021-06-15 21:40:14 +08:00
RigoLigoRLC
479d218651 New Non-DTK store client prototype 2021-06-15 21:39:04 +08:00
0ab9f4dda6 !22 合并 multiple 分支修复的 BUG
Merge pull request !22 from zty199/multiple
2021-06-13 14:36:48 +08:00
5a97f6caef Merge branch 'master' of gitee.com:deepin-community-store/spark-store into multiple 2021-06-13 14:36:05 +08:00
3da25b0904 Fix bugs about server address in widget.cpp 2021-06-13 14:30:31 +08:00
RigoLigo
116c824365 更正错误的服务URL 2021-06-09 20:40:45 +08:00
zty199
44edb08518 Fix issues about TitleBar
Fix a issue about titlebar display on Ubuntu.
2021-04-24 21:17:51 +08:00
zty199
9a3d32ee11 Improve Features
Support save current theme setting when exit;
Use QScreen::primaryScreen() instead of QDesktopWidget to get desktop size;
Ensure only one instance will be running at the same time.
2021-04-20 18:49:17 +08:00
Jerry
e5fe80cb76 add jenkins from master 2021-04-20 16:04:42 +08:00
Jerry
9314acd7e1 !21 Jenkins自动构建并在commit下评论
* debug
* update
* debug
* debug
* debug
* debug
* debug
* update
* update
* debug
* Merge branch 'jenkins' of gitee.com:deepin-community-store/spark-store…
* up
* Added Jenkinsfile
* debug
* debug
* debug
* up
* up
* debug
* up
* debug
* Merge branch 'jenkins' of gitee.com:deepin-community-store/spark-store…
* debug
* Added Jenkinsfile
* Added Jenkinsfile
* up
* 1
* update
* Added Jenkinsfile
* Merge branch 'jenkins' of gitee.com:deepin-community-store/spark-store…
* update
* Added Jenkinsfile
* Added Jenkinsfile
* Added Jenkinsfile
* Added Jenkinsfile
* 不使用 make -j
* 修改字体
* Added Jenkinsfile
* Added Jenkinsfile
2021-04-20 15:50:44 +08:00
zty199
e64e7fcae2 Reorganize codes
Reorganize all parts of codes;
Fix a bug that index page color is not correct when initialized.
2021-04-17 15:43:11 +08:00
536efeedfa !19 检查 UI 中的 BUG
* 整理部分代码
* 修改下载线程上限 4 线程为 5 线程
2021-04-17 04:37:30 +08:00
zty199
6fd3c40e97 Reorganize part of codes
Reorganize spark-store.pro, widget.h and widget.cpp.
2021-04-17 04:30:28 +08:00
Jerry
90513a8925 !18 4线程到5线程
Merge pull request !18 from Jerry/4to5
2021-04-17 03:23:01 +08:00
Jerry
ed3d869b99 4线程切换到5线程 2021-04-17 03:22:03 +08:00
Jerry
c80737a458 Merge branch 'multiple' of gitee.com:deepin-community-store/spark-store into multiple 2021-04-17 03:19:44 +08:00
Jerry
ebf30e67f9 4线程到5线程 2021-04-17 03:19:38 +08:00
Jerry
90b684af87 Merge branch 'master' of gitee.com:deepin-community-store/spark-store into multiple 2021-04-17 03:18:40 +08:00
Jerry
3ff363d7b1 4线程切到5线程 2021-04-17 03:16:22 +08:00
Jerry
f7ced7739c !17 多线程下载合并
* Update README.md
* Improve Features
* 修改获取线路的域名
* 修改默认源
* 修改多线程下载域名和图片服务器
* 修正取消下载的闪退问题
* 将图片下载由 curl 转为 QtNetworkService
* 切换多域名下载,提高下载速度
* 完成并发请求下载
2021-04-17 01:35:51 +08:00
zty199
e12f617f59 Update README.md 2021-04-17 01:16:01 +08:00
zty199
d164aec86d Improve Features
Fix a bug that annotations in server list can be chosen in combobox;
Fix a bug that index page don't follow system theme;
Disable qDebug/qWarning output in Release version;
Update default server list.
2021-04-17 00:53:26 +08:00
Jerry
96cd1b9918 修改获取线路的域名 2021-04-16 19:05:30 +08:00
Jerry
393c8220f5 修改默认源 2021-04-16 18:48:01 +08:00
Jerry
01d1543cc4 修改多线程下载域名和图片服务器 2021-04-16 12:19:22 +08:00
metanoia1989
4ccc8c0dae 修正取消下载的闪退问题 2021-03-06 17:00:55 +08:00
metanoia1989
815036e28f 将图片下载由 curl 转为 QtNetworkService 2021-03-06 15:18:31 +08:00
metanoia1989
9cc68fac86 切换多域名下载,提高下载速度 2021-02-20 06:47:57 +08:00
metanoia1989
2f8c11a30b 完成并发请求下载 2021-02-16 23:00:27 +08:00
1a4b1176fb !16 Improve Features
* Improce Features
* History Backup for Version 3.0-stable
2021-01-30 21:12:20 +08:00
3101f1fe70 Restore Patches 2020-12-17 00:18:10 +08:00
58f32c119a Bump version to 3.0 2020-12-16 23:49:54 +08:00
ccdcf407cc 调整安装选项顺序
调整安装选项顺序;
更新服务器源优先级;
重新打包。
2020-12-15 00:30:26 +08:00
1a18a51d3c !15 修正搜索功能的几个BUG
Merge pull request !15 from 枯叶蚊/search
2020-12-12 14:48:02 +08:00
metanoia1989
ab88af006b 修正搜索列表应用重复点击进入失败的问题 2020-12-10 22:46:55 +08:00
metanoia1989
d02900cb10 Merge branch 'search' of gitee.com:deepin-community-store/spark-store into search
合并主分支的搜索图标
2020-12-10 21:32:15 +08:00
metanoia1989
3e473c091a 解决从详情页返回列表页的进度恢复问题 2020-12-10 21:31:53 +08:00
shenmo
84541d0c22 修改: translations/spark-store_zh_CN.ts 2020-12-10 16:41:25 +08:00
shenmo
51f84bed1b 修改: src/downloadlist.cpp
修改:     src/spark-store.pro
	删除:     trans/spark-store_en.qm
	删除:     trans/spark-store_fr.qm
	删除:     trans/spark-store_zh_CN.qm
	重命名:   trans/spark-store_en.ts -> translations/spark-store_en.ts
	重命名:   trans/spark-store_fr.ts -> translations/spark-store_fr.ts
	重命名:   trans/spark-store_zh_CN.ts -> translations/spark-store_zh_CN.ts
2020-12-10 16:40:42 +08:00
4a0acf0575 修改: assets/icons/category_active.svg
新文件:   assets/icons/category_active.svg.bak
	修改:     assets/icons/category_active_dark.svg
	新文件:   assets/icons/category_active_dark.svg.bak
2020-12-10 12:35:14 +08:00
metanoia1989
e28d1c39ac 返回列表不重新加载 2020-12-08 23:31:40 +08:00
metanoia1989
f58201a612 合并来自主干的分支 2020-12-08 22:48:27 +08:00
405d3b6986 Try to fix bugs
尝试修复多屏幕下截图全屏预览的偏移问题。
2020-12-06 12:32:09 +08:00
b619d3cc7b Fix Bugs
修复了搜索结果页存在页边距的问题;
修复了多次搜索同时触发导致的崩溃问题。
2020-12-05 18:45:47 +08:00
枯叶蚊
14e3e7f9a2 !14 星火商店搜索功能
* 更换搜索服务器域名为星火的域名
* 更新搜索服务器为线上服务器
* 完成搜索功能
* 解决搜索结果图标锯齿问题
* 更新appitem的样式
* 完成应用搜索列表的滚动问题
* 合并master分支
* 添加一些文件到忽略列表
* 更新项目结构
* 更新搜索列表UI
* 添加 QtNetworkService库
2020-12-05 16:06:52 +08:00
metanoia1989
1d0e0cc65c 更换搜索服务器域名为星火的域名 2020-12-05 13:53:13 +08:00
metanoia1989
7a5b982dea 更新搜索服务器为线上服务器 2020-12-02 22:22:59 +08:00
metanoia1989
91fcab56df 完成搜索功能 2020-12-01 22:10:52 +08:00
metanoia1989
4315f04023 解决搜索结果图标锯齿问题 2020-11-30 22:44:25 +08:00
metanoia1989
736ede0742 更新appitem的样式 2020-11-29 22:25:20 +08:00
metanoia1989
a73a4416fc 完成应用搜索列表的滚动问题 2020-11-29 20:28:59 +08:00
shenmo
62f0dd097c update README.md. 2020-11-29 12:11:42 +08:00
metanoia1989
6f3e4398df 合并master分支 2020-11-28 20:59:22 +08:00
metanoia1989
3cca0d87fe 添加一些文件到忽略列表 2020-11-21 11:18:20 +08:00
b7e038bd88 Fix Bugs
Fix a bug that dirname in ~/.local/share/spark-union/ is translated into
Chinese.
2020-11-19 14:16:16 +08:00
shenmo
4f0e00ad76 修改了一个图标的名字 2020-11-18 15:17:36 +08:00
shenmo
579008e8b2 !13 更新部分语言翻译
Merge pull request !13 from 枯叶蚊/master
2020-11-14 12:17:36 +08:00
metanoia1989
ccb405c983 更新部分语言翻译 2020-11-14 11:02:50 +08:00
metanoia1989
66ef37c1ca 更新项目结构 2020-11-13 22:57:37 +08:00
metanoia1989
8972425c7c 更新搜索列表UI 2020-11-08 22:35:31 +08:00
metanoia1989
2ae6e80785 添加 QtNetworkService库 2020-11-08 20:41:18 +08:00
f5788efb47 update README.md. 2020-10-27 17:26:56 +08:00
32bc272791 update README.md. 2020-10-27 17:26:12 +08:00
c5f04b5675 update README.md. 2020-10-27 17:24:50 +08:00
cf208d0736 Roll back to old version of DAboutDialog
Roll back to old version of DAboutDialog for compability with Ubuntu.
2020-10-27 17:15:08 +08:00
fdc7cb4cb6 修改: README.md 2020-10-27 16:55:56 +08:00
8e89ce3ae3 keep old version AboutDialog
Keep old version AboutDialog to be compatible with ubuntu.
2020-10-26 23:29:41 +08:00
ee51f59874 Bump version to 2.0.2.5
Bump version to 2.0.2.5.
2020-10-26 22:50:21 +08:00
3d09a28794 main.coo改回定制,2.0.2.5版本号,改了logo配色 2020-10-26 17:47:24 +08:00
4b05758479 main.coo改回定制,2.0.2.5版本号,改了logo配色 2020-10-26 17:47:14 +08:00
74d9d0b563 main.coo改回定制,2.0.2.5版本号,改了logo配色 2020-10-26 17:44:19 +08:00
123456
f1a4f7acb5 main.cpp改回spark定制 2020-10-26 17:41:46 +08:00
93be66e871 Improve menu list on the left
Set StyleSheet of menu list on the left to
"text-align:left;padding-left:15px;" when system locale is not "zh_CN";

Update part of names in menu list and corresponding translation.
2020-10-24 20:24:31 +08:00
7b46cca1d4 Fix Bugs
Fix a Bug in updateUI(), everytime calling this method will set fonts to
system default display font and delete custom font settings, have to set
custom fonts again here.
2020-10-23 15:15:53 +08:00
c529367998 !12 卸载安装包时,将pkg转换为小写,让dpkg能够识别
Merge pull request !12 from 枯叶蚊/master
2020-10-20 09:02:27 +08:00
metanoia1989
beae3e3efa 应用下载后面换行 2020-10-19 22:44:44 +08:00
metanoia1989
9706480931 添加.vscode和build为忽略目录 2020-10-19 22:17:22 +08:00
metanoia1989
ed64eb6f5b 卸载安装包时,将pkg-name转换为小写 2020-10-19 22:16:59 +08:00
8f6e5408ae 修改: main.cpp,使用了旧式的DAboutDialog,放弃了定制,可以在ubuntu上运行了。仍然无法打开关于 2020-10-19 15:16:54 +08:00
d5783458fc update README.md. 2020-10-19 00:14:10 +08:00
6e05066dd9 update README.md. 2020-10-19 00:11:33 +08:00
58aa5a3787 update README.md. 2020-10-19 00:10:21 +08:00
6087c8a5c8 update README.md. 2020-10-18 10:13:06 +08:00
ff758946be update README.md. 2020-10-18 10:12:30 +08:00
d1cbf95447 update README.md. 2020-10-18 10:11:18 +08:00
Jerry
187ce7e277 编译说明增加一个组件 2020-10-16 15:07:18 +08:00
d3e4f75254 !11 修复中文环境无法显示通知的问题
Merge pull request !11 from lidanger/master
2020-10-15 15:12:53 +08:00
lidanger
949eb33511 修复中文环境通知,上次少加两行初始化代码 2020-10-14 22:20:55 +08:00
lidanger
3f65002dc9 修复中文环境无法显示通知的问题 2020-10-14 20:57:32 +08:00
RigoLigoRLC
cfd4c7689a RigoLigo合并随机崩溃的修复 2020-10-09 02:08:01 +08:00
RigoLigoRLC
367c8d857c 修复 加载应用程序信息时随机崩溃
这个问题就是因为在异步操作时直接修改GUI,导致事件发生时GUI与异步加载线程不同步导致程序崩溃。将所有加载操作已移至
SpkAppInfoLoaderThread,在其需要操作GUI时阻塞该工作线程并发射信号让主线程操作GUI,以解决该问题。
2020-10-09 02:06:28 +08:00
b41f846ea4 2.0.2.3,修复depend 2020-10-07 22:08:13 +08:00
1dcd7d1c5c fr 2020-10-06 19:48:14 +08:00
01ac388f50 更改qm名字适配dtk 2020-10-06 17:25:25 +08:00
cfbbd34695 !9 add trans/lang_French.ts.
Merge pull request !9 from App4Deepin/N/A
2020-10-06 09:33:16 +08:00
App4Deepin
b7afc5ec8a add trans/lang_French.ts. 2020-10-05 23:34:27 +08:00
b911158274 修改: trans/spark-store_en.ts
修改:     trans/spark-store_zh_CN.qm,支持了贡献翻译
	修改:     trans/spark-store_zh_CN.ts,支持贡献翻译
	修改:     widget.cpp,微调原文,天加两个tips
2020-10-05 22:11:38 +08:00
67c28d0224 删除构建文件 2020-10-05 22:04:13 +08:00
1d65bfc66d 版本号不再翻译,贡献翻译按钮翻译。to-do:翻译按界面翻译 2020-10-05 19:49:37 +08:00
0e595db328 logo 2020-10-05 10:44:20 +08:00
7a16028e38 logo 2020-10-05 10:44:08 +08:00
RigoLigoRLC
1b56d3ad52 更改贡献翻译按钮提示框为DDialog,并修复遗留翻译问题 2020-10-04 00:13:06 +08:00
431e7b555f dwine5-dwine2-tags 2020-10-03 11:26:21 +08:00
RigoLigoRLC
1cbabe7cc5 详情页面添加翻译按钮 2020-10-03 01:30:05 +08:00
d027d513a3 微调界面 2020-10-02 21:33:03 +08:00
RigoLigoRLC
eff8878e8b 修复下载软件失败导致的崩溃,更改APT源更新逻辑
下载失败会导致发射的progress信号已接受数和总数均为0,导致计算进度时被零除;
APT源更新逻辑代码重构,将需要复用的值单独创建变量,重新格式化代码,以及加入了已经废弃的星火源降低优先级代码
2020-10-02 12:03:40 +08:00
f0bf0e5aec 微调翻译,增添翻译 2020-10-01 10:12:13 +08:00
RigoLigoRLC
678a4229f7 将通知移至专用函数,并更新中文翻译ts文件 2020-10-01 02:53:10 +08:00
e89c14b5ca QObject::tr---update-ubuntu-tag 2020-09-30 15:39:30 +08:00
RigoLigoRLC
d4c734627a 添加了用户端QtCreator配置缓存的忽略 2020-09-30 01:07:42 +08:00
8830b14e10 translate#4 2020-09-29 19:49:35 +08:00
8c9f5b8ba6 尝试添加tag.Trying to add tags 2020-09-29 17:44:55 +08:00
badc3aa089 translate-to-English 2020-09-29 17:18:43 +08:00
9c9317dea0 更多的翻译 2020-09-29 12:37:49 +08:00
594f71f98a 英文 2020-09-28 19:50:39 +08:00
c45bf8ffaf !8 菜单新增“软件投稿”选项,添加刷新按钮
Merge pull request !8 from RigoLigo/master
2020-09-28 11:04:35 +08:00
RigoLigoRLC
c9c0357646 新增刷新按钮(和WebEngineView的右键菜单并存) 2020-09-27 15:02:41 +08:00
RigoLigoRLC
bd8f660fe9 菜单中加入投稿按钮,拉起默认浏览器进入投稿
https://gitee.com/deepin-community-store/spark-store/issues/I1VD2Q
2020-09-27 02:06:40 +08:00
538c742303 !7 新增了下载app.json或图标失败将给用户发送提醒
Merge pull request !7 from RigoLigo/master
2020-09-26 10:36:42 +08:00
RigoLigoRLC
c23e816017 新增 下载app.json或图标失败将给用户发送提醒
这里改变了widget这个类里面loadappinfo的返回值,使处理异常返回状态可以实现
2020-09-26 01:19:57 +08:00
2be0e1b523 2.0+1发布 2020-09-23 17:32:26 +08:00
eacc56eaf3 完全改好了 2020-09-23 17:30:46 +08:00
9458bbf2c9 更新DAboutDialog 2020-09-23 16:08:44 +08:00
moshengren
e472ba76f1 !6 Update of english translations
Merge pull request !6 from alvarosamudio/master
2020-09-19 10:20:37 +08:00
alvarosamudio
74bd3ae018 update trans/lang_English.ts. 2020-09-18 23:57:40 +08:00
Jerry
ccc2bb8898 更新README 2020-09-18 15:46:43 +08:00
a59e2e0527 fanyi 2020-09-18 12:36:42 +08:00
2c205bf5db 翻译 2020-09-17 16:49:23 +08:00
efc6c50f88 微调DAboutDialogue内容 2020-09-12 13:36:37 +08:00
ce263c3cb4 更新翻译 2020-09-12 13:29:34 +08:00
78ac339629 翻译更新 2020-09-11 13:36:29 +08:00
58359aa63a !5 更新了图片服务器 线路说明转移到服务端上
Merge pull request !5 from Jerry/master
2020-09-08 17:20:58 +08:00
jerry
e839f7836f 不在客户端现实线路说明, 线路说明写到server.list中 2020-09-08 15:31:55 +08:00
jerry
b34c50d2e0 图片服务器使用jsdelivr cdn 2020-09-08 14:19:47 +08:00
jerry
4d4e3e3e3a 更新图片服务器 2020-09-05 18:20:14 +08:00
jerry
10741a1d92 更新了线路说明, 更具有普适性 2020-09-03 18:40:04 +08:00
maicss
b78ca0e103 新增 软件标签 2020-09-03 10:05:48 +08:00
maicss
3a0ed16a21 Merge branch 'master' of https://gitee.com/deepin-community-store/spark-store 2020-09-02 14:28:52 +08:00
maicss
0a3d583d76 优化UI细节 2020-09-02 14:28:09 +08:00
张天怿
f5121a0405 Delete .pro.user files
Delete useless .pro.user files.
2020-09-01 11:03:20 +08:00
Maicss
e568ddafba 更新 文档
Merge pull request !2 from Jerry/master
2020-08-29 07:42:02 +08:00
jerry
111174a46f update 2020-08-29 00:43:36 +08:00
jerry
6de25a299f update readme 2020-08-29 00:41:23 +08:00
jerry
cbd57a3e25 增加线路说明, 如何编译的说明 2020-08-29 00:27:02 +08:00
maicss
6e083f295b 新增 深度安装器安装选项 2020-08-28 15:03:10 +08:00
maicss
b82a821d01 修复UI细节问题 2020-08-28 14:45:39 +08:00
Maicss
6578af935e 优化UI 2020-08-26 19:58:42 +08:00
Maicss
fe4143a3f7 修改 web控件 2020-08-26 09:39:55 +08:00
Maicss
8952be33c4 修改 web控件 2020-08-26 09:27:34 +08:00
Maicss
4e4f55995e 修改 web控件 2020-08-26 09:26:59 +08:00
张天怿
7af2af64d8 Update image server
Change image server.
2020-08-24 12:44:03 +08:00
张天怿
e7276662f4 Add new server
Change default serverURL for better usage exxperience.
2020-08-23 20:11:02 +08:00
4138c5d634 bug 2020-08-22 09:38:49 +08:00
2bb4feec43 7+2——更改源服务器 2020-08-21 17:29:05 +08:00
Maicss
18c3965742 追加 上一条 2020-08-10 12:36:00 +08:00
Maicss
647fb20f1e 删除 标题栏分割线
修复 下载列表间隙问题
2020-08-10 12:34:51 +08:00
Maicss
a75ecd3a9a 增强 稳定性 2020-08-10 09:27:07 +08:00
Maicss
f9b8faa638 优化 UI细节 2020-08-09 09:08:39 +08:00
Maicss
f39890240f 添加 返回列表按钮 2020-08-08 09:12:46 +08:00
Maicss
3ca8fac6f9 新增 列表ui图标 2020-08-07 10:32:38 +08:00
Maicss
9fbe0032b7 优化 截图的缓存机制 2020-08-06 20:03:17 +08:00
Maicss
3ba66f8f8e update 2020-08-06 16:55:06 +08:00
Maicss
cf1322bab4 删除 无用文件 2020-08-05 20:03:48 +08:00
Maicss
135d16d068 修改 版本号到beta7 2020-08-05 20:00:13 +08:00
Maicss
44201a8bed 修复 低分辨率下默认只显示1列 2020-08-05 19:58:23 +08:00
Maicss
8fafd7b3ee 大幅修改UI设计 2020-08-05 19:49:39 +08:00
Maicss
070788d50d 新增 黑色模式 2020-08-05 10:35:50 +08:00
Maicss
0e387acfb6 修复ssinstall安装参数问题 2020-08-04 14:41:57 +08:00
Maicss
c043542c04 更换 详情页专有服务器 2020-08-04 13:34:42 +08:00
Maicss
25b37ec0f2 更新 2020-08-04 10:27:06 +08:00
Maicss
a732269b62 新增 安装方式选择 2020-08-04 10:01:05 +08:00
Maicss
6d63ef60ac 新增 安装方式选择 2020-08-04 09:57:21 +08:00
张天怿
25456a413c 添加“手动安装”按钮
添加手动安装按钮来处理部分 gdebi 无法正确安装的应用(主要是 wine 应用)。
2020-08-03 18:25:08 +08:00
Maicss
ec1c91f021 修复 截图放大后可能会溢出屏幕 2020-07-29 20:51:18 +08:00
Maicss
46c1609d58 修改 截图预览背景参数 2020-07-28 17:31:00 +08:00
Maicss
9c5421133f 新增 预览截图时背景模糊
新增 打开下载文件夹功能
修复 取消下载被判定为下载失败
2020-07-28 17:08:47 +08:00
Maicss
114489c3d6 重写了截图查看窗口 2020-07-28 13:58:48 +08:00
Maicss
101a6bc370 修复 因ObjectName更改导致无法自动连接的问题 2020-07-28 13:04:49 +08:00
Maicss
9b47e4dd70 修改 左侧菜单为QPushButton 2020-07-28 12:56:15 +08:00
Maicss
6a555b06b9 修改 左侧菜单跟随系统活动色 2020-07-28 11:48:40 +08:00
Maicss
145b1b8e30 修复 下载未完成时断网,会显示下载成功 2020-07-28 10:38:03 +08:00
Maicss
23d8e9ea82 更新 版本号 2020-07-28 08:26:06 +08:00
Maicss
f723007166 优化 UI 2020-07-27 12:05:40 +08:00
Maicss
e0639a5855 修改 搜索框事件 2020-07-27 10:26:12 +08:00
Maicss
514be30e04 新增 链接分享 2020-07-27 10:20:50 +08:00
Maicss
817d497f4a 更新 文档中Url列表错误 2020-07-27 09:41:26 +08:00
8158159bf9 跟随 2020-07-26 11:32:48 +08:00
82f49c4aa9 跟随 2020-07-24 10:00:05 +08:00
b1c2b1bbc6 跟随到5.2 2020-07-13 19:26:09 +08:00
892766470c 跟随到beta5 2020-07-09 09:05:50 +08:00
a3ca04ddc5 更新到spark,加入apt-list的修改功能 2020-07-04 12:09:35 +08:00
147258a7ac SparkProject 2020-07-03 20:53:58 +08:00
68269577c0 beta3更新 2020-07-03 11:53:47 +08:00
58304f2c19 优化了app.json解析 2020-07-01 20:54:42 +08:00
130 changed files with 12093 additions and 2527 deletions

2
.gitee/Dockerfile Normal file
View File

@@ -0,0 +1,2 @@
FROM python:3
RUN pip3 install requests

31
.gitee/callback.py Normal file
View File

@@ -0,0 +1,31 @@
import os
import requests
import json
# sha=os.system("git rev-parse HEAD")
sha = os.getenv("GIT_COMMIT")
# sha = '48fed26c51a8c42554e45f72f43e49703e04c97f'
#get sha from environment
url = "https://gitee.com/api/v5/repos/deepin-community-store/spark-store/commits/{}/comments".format(sha)
token = os.getenv("gitee_token")
# process = os.popen("git symbolic-ref --short -q HEAD")
body = "构建详情请见" + os.getenv("JENKINS_URL") + "blue/organizations/jenkins/" + os.getenv("JOB_NAME").replace("/", "/detail/") + "/" + str(os.getenv("BUILD_ID"))
# process.close()
d = {
'access_token': token,
"body": body
}
h = {
"Content-Type": "application/json;charset=UTF-8"
}
res = requests.post(url,headers=h, data=json.dumps(d))
# print(res.status_code)
# print(res.content)

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.pro.user*
build/
.vscode/
Lib/

143
CMakeLists.txt Normal file
View File

@@ -0,0 +1,143 @@
cmake_minimum_required(VERSION 3.10)
project(spark_store)
set(EXECUTABLE_NAME spark-store)
# Begin Compilation Options
# When set to ON, DTK Plugin for DDE platform will be built.
# Note that only machines with DTK and dev packages installed can build the plugin.
set(SPARK_BUILD_DTK_PLUGIN ON)
# When set to ON, store will transmit telemetry even in Debug builds.
set(SPARK_FORCE_TELEMETRY OFF)
# End Compilation Options
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
#set(CMAKE_AUTOUIC ON)
set(QT_VERSION 5)
set(REQUIRED_LIBS
Core
Gui
Widgets
Network
Concurrent
LinguistTools)
set(REQUIRED_LIBS_QUALIFIED
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Network
Qt5::Concurrent)
include_directories(inc)
include_directories(plugin)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(PRE_CONFIGURE_FILE "src/gitver.cpp.in")
set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/src/gitver.cpp")
include(cmake/git_watcher.cmake)
add_library(gitver STATIC ${POST_CONFIGURE_FILE})
#target_include_directories(gitver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies(gitver check_git)
set(SOURCE_FILES
src/main.cpp
resource/resource.qrc
gui/page/ui/settings.ui
gui/page/ui/homepage.ui
${WRAPPED_UI_FILES}
inc/gitver.h
inc/deepinplatform.h
inc/dtk/spkdtkplugin.h
inc/spkutils.h src/spkutils.cpp
inc/telemetry/collectid.h
inc/spkqsshelper.h gui/spkqsshelper.cpp
inc/qt/elidedlabel.h gui/qt/elidedlabel.cpp
inc/spkwindow.h gui/spkwindow.cpp
inc/spktitlebar.h gui/spktitlebar.cpp
inc/spkiconbutton.h gui/spkiconbutton.cpp
inc/spkui_general.h gui/spkui_general.cpp
inc/spkmainwindow.h gui/spkmainwindow.cpp
inc/spkmsgbox.h gui/spkmsgbox.cpp
inc/spkdialog.h gui/spkdialog.cpp
inc/spkabout.h gui/spkabout.cpp
inc/spkloading.h gui/spkloading.cpp
inc/spksidebartree.h gui/spksidebartree.cpp
inc/spkappitem.h gui/spkappitem.cpp
inc/spkdownloadentry.h gui/spkdownloadentry.cpp
inc/spkpopup.h gui/spkpopup.cpp
inc/spkstretchlayout.h gui/spkstretchlayout.cpp
inc/spkfocuslineedit.h
inc/spknotifydot.h gui/spknotifydot.cpp
inc/spkimgviewer.h gui/spkimgviewer.cpp
inc/page/spkpagebase.h gui/page/spkpagebase.cpp
inc/page/spkpagehome.h gui/page/spkpagehome.cpp
inc/page/spkpageuitest.h gui/page/spkpageuitest.cpp
inc/page/spkpageapplist.h gui/page/spkpageapplist.cpp
inc/page/spkpageappdetails.h gui/page/spkpageappdetails.cpp
inc/page/spkpagedownloads.h gui/page/spkpagedownloads.cpp
inc/page/spkpagesettings.h gui/page/spkpagesettings.cpp
inc/pkgs/spkpkgmgrbase.h
inc/pkgs/spkpkgmgrpacman.h src/pkgs/spkpkgmgrpacman.cpp
inc/pkgs/spkpkgmgrapt.h src/pkgs/spkpkgmgrapt.cpp
inc/spkstore.h src/spkstore.cpp
inc/spkuimsg.h src/spkuimsg.cpp
inc/spklogging.h src/spklogging.cpp
inc/spkresource.h src/spkresource.cpp
inc/spkdownload.h src/spkdownload.cpp
inc/spkconfig.h src/spkconfig.cpp
)
include(cmake/FindLibNotify.cmake)
include(cmake/FindGlib.cmake)
include(cmake/FindGdk3.cmake)
include_directories(${GLIB_INCLUDE_DIRS})
include_directories(${GDK3_INCLUDE_DIRS})
set(LIBLINKING ${LIBLINKING}
gitver
${LIBNOTIFY_LIBRARIES}
${GLIB_LIBRARIES}
${GDK3_LIBRARIES}
${CMAKE_DL_LIBS})
# Required for a good backtrace
add_compile_options(-g)
add_link_options(-rdynamic)
# Find Qt before adding other build targets
find_package(Qt${QT_VERSION} COMPONENTS ${REQUIRED_LIBS} REQUIRED)
QT5_WRAP_UI(WRAPPED_UI_FILES
gui/page/ui/settings.ui
gui/page/ui/homepage.ui)
add_custom_target(run_lupdate
COMMAND lupdate . -ts lang/zh.ts
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
qt5_add_translation(QM_FILES lang/zh.ts)
add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES} ${QM_FILES})
if(SPARK_BUILD_DTK_PLUGIN)
add_subdirectory(plugin/dtkplugin)
add_dependencies(${EXECUTABLE_NAME} spkdtkplugin)
endif()
target_link_libraries(${EXECUTABLE_NAME} ${REQUIRED_LIBS_QUALIFIED} ${LIBLINKING})
install(TARGETS ${EXECUTABLE_NAME} RUNTIME DESTINATION bin)

33
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,33 @@
pipeline {
agent any
stages {
stage('build') {
agent {
docker {
image 'jerry979/dtke:5.11'
}
}
steps {
sh 'mkdir build && cd build && cmake .. && make -j'
archiveArtifacts(artifacts: 'build/src/spark-store', allowEmptyArchive: true, defaultExcludes: true)
}
}
stage('send') {
agent {
dockerfile {
filename '.gitee/Dockerfile'
}
}
environment {
gitee_token = credentials('1')
}
steps {
sh "python3 .gitee/callback.py"
}
}
}
}

BIN
Logo-Spark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
Logo-Spark.png.bak Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,33 +1,73 @@
# 深度社区应用商店
# 星火应用商店
众所周知国内的Linux应用比较少wine应用难以获取优质工具分散在民间各大论坛无法形成合力难以改善生态
生态构建需要的不是某一方的单打独斗,而是人人行动起来,汇聚星火,产生燎原之势
我们创建了这个应用商店广泛收录大家需要的软件包搜集优质小工具主动适配wine应用存放到储存库供大家获取
我们支持Deepin 20 ; Ubuntu 20.04 LTS ; UOS Home 20
希望看到这里的人也可以加入我们的队伍开发或者投递应用都很欢迎共同构建Linux应用生态
### [在这里投稿](http://upload.spark-app.store)
#### 介绍
deepin社区商店由社区维护
web页面部分正在开发当中详情请见[web仓库](https://gitee.com/deepin-community-store/DCSAPP_WEB)
#### 编译指导
依赖dtk2版适用于ubuntu/15.11)qt5-default g++ libdtkcore2-dev libdtkwidget2-dev libqt5webkit5-dev libdtkgui2-dev
dtk5版适用于20beta):qt5-default g++ libdtkwidget5-dev libdtkcore5-dev libdtkgui5-dev libqt5webkit5-dev
#### 说明
需要在运行目录下放置服务器线路列表`server.list`,每行一个,在末尾需要添加“/”
当前服务器线路列表(项目中包含):
```
http://dcstore.shenmo.tech/
http://store.shenmo.tech/
http://store2.shenmo.tech/
http://store.moshengren.xyz/
https://d.store.deepinos.org.cn/
https://store.deepinos.org.cn/
```
#### 目录结构
#### 调用参数(spk规则)
安装软件过程中产生的包,图标,截图等被储存到`/tmp/deepin-community-store/`中。
参数只有一个Url该url应当遵循这种格式`spk://<任意合法字符>/web分类/包名`
配置文件被储存到`~/.config/deepin-community-store/`中。
例如:
线路文件:当前运行目录下的`./server.list`
[spk://abcdefg/games/store.spark-app.hmcl](spk://abcdefg/games/store.spark-app.hmcl)
#### 参与贡献
1. Fork 本仓库
可选的web分类
| 分类名称 | web分类   |
| -------- | -------------- |
| 网络应用 | network |
| 社交沟通 | chat |
| 音乐欣赏 | music |
| 视频播放 | video |
| 图形图像 | graphics |
| 游戏娱乐 | games |
| 办公学习 | office |
| 阅读翻译 | reading |
| 编程开发 | development |
| 系统工具 | tools |
| 主题美化 | beautify |
| 其他应用 | others |
#### 如何编译
Deepin V20/UOS 系统下, 安装依赖
```shell
sudo apt install qt5-default libdtkcore-dev libdtkwidget-dev qtwebengine5-dev libnotify-dev
```
```shell
git clone https://gitee.com/deepin-community-store/spark-store.git
cd spark-store
mkdir build
cd build
qmake ..
make -j
```
./build文件下的spark-store即为可执行文件

52
cmake/FindGdk3.cmake Normal file
View File

@@ -0,0 +1,52 @@
# - Try to find GDK 3
# Once done, this will define
#
# GDK3_FOUND - system has GDK 3
# GDK3_INCLUDE_DIRS - the GDK 3 include directories
# GDK3_LIBRARIES - link these to use GDK 3
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
# Copyright (C) 2013 Igalia S.L.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Source from:
# https://sources.debian.org/src/qtwebkit-opensource-src/5.212.0%7Ealpha2-21/Source/cmake/FindGDK3.cmake/
find_package(PkgConfig)
pkg_check_modules(GDK3 gdk-3.0)
set(VERSION_OK TRUE)
if (GDK3_VERSION)
if (GDK3_FIND_VERSION_EXACT)
if (NOT("${GDK3_FIND_VERSION}" VERSION_EQUAL "${GDK3_VERSION}"))
set(VERSION_OK FALSE)
endif ()
else ()
if ("${GDK3_VERSION}" VERSION_LESS "${GDK3_FIND_VERSION}")
set(VERSION_OK FALSE)
endif ()
endif ()
endif ()
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(GDK3 DEFAULT_MSG GDK3_INCLUDE_DIRS GDK3_LIBRARIES VERSION_OK)

125
cmake/FindGlib.cmake Normal file
View File

@@ -0,0 +1,125 @@
# - Try to find Glib and its components (gio, gobject etc)
# Once done, this will define
#
# GLIB_FOUND - system has Glib
# GLIB_INCLUDE_DIRS - the Glib include directories
# GLIB_LIBRARIES - link these to use Glib
#
# Optionally, the COMPONENTS keyword can be passed to find_package()
# and Glib components can be looked for. Currently, the following
# components can be used, and they define the following variables if
# found:
#
# gio: GLIB_GIO_LIBRARIES
# gobject: GLIB_GOBJECT_LIBRARIES
# gmodule: GLIB_GMODULE_LIBRARIES
# gthread: GLIB_GTHREAD_LIBRARIES
#
# Note that the respective _INCLUDE_DIR variables are not set, since
# all headers are in the same directory as GLIB_INCLUDE_DIRS.
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Source from:
# https://sources.debian.org/src/qtwebkit-opensource-src/5.212.0%7Ealpha2-21/Source/cmake/FindGLIB.cmake/
find_package(PkgConfig)
pkg_check_modules(PC_GLIB QUIET glib-2.0)
find_library(GLIB_LIBRARIES
NAMES glib-2.0
HINTS ${PC_GLIB_LIBDIR}
${PC_GLIB_LIBRARY_DIRS}
)
# Files in glib's main include path may include glibconfig.h, which,
# for some odd reason, is normally in $LIBDIR/glib-2.0/include.
get_filename_component(_GLIB_LIBRARY_DIR ${GLIB_LIBRARIES} PATH)
find_path(GLIBCONFIG_INCLUDE_DIR
NAMES glibconfig.h
HINTS ${PC_LIBDIR} ${PC_LIBRARY_DIRS} ${_GLIB_LIBRARY_DIR}
${PC_GLIB_INCLUDEDIR} ${PC_GLIB_INCLUDE_DIRS}
PATH_SUFFIXES glib-2.0/include
)
find_path(GLIB_INCLUDE_DIR
NAMES glib.h
HINTS ${PC_GLIB_INCLUDEDIR}
${PC_GLIB_INCLUDE_DIRS}
PATH_SUFFIXES glib-2.0
)
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR} ${GLIBCONFIG_INCLUDE_DIR})
# Version detection
if (EXISTS "${GLIBCONFIG_INCLUDE_DIR}/glibconfig.h")
file(READ "${GLIBCONFIG_INCLUDE_DIR}/glibconfig.h" GLIBCONFIG_H_CONTENTS)
string(REGEX MATCH "#define GLIB_MAJOR_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}")
set(GLIB_VERSION_MAJOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define GLIB_MINOR_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}")
set(GLIB_VERSION_MINOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define GLIB_MICRO_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}")
set(GLIB_VERSION_MICRO "${CMAKE_MATCH_1}")
set(GLIB_VERSION "${GLIB_VERSION_MAJOR}.${GLIB_VERSION_MINOR}.${GLIB_VERSION_MICRO}")
endif ()
# Additional Glib components. We only look for libraries, as not all of them
# have corresponding headers and all headers are installed alongside the main
# glib ones.
foreach (_component ${GLIB_FIND_COMPONENTS})
if (${_component} STREQUAL "gio")
find_library(GLIB_GIO_LIBRARIES NAMES gio-2.0 HINTS ${_GLIB_LIBRARY_DIR})
set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GIO_LIBRARIES)
elseif (${_component} STREQUAL "gobject")
find_library(GLIB_GOBJECT_LIBRARIES NAMES gobject-2.0 HINTS ${_GLIB_LIBRARY_DIR})
set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GOBJECT_LIBRARIES)
elseif (${_component} STREQUAL "gmodule")
find_library(GLIB_GMODULE_LIBRARIES NAMES gmodule-2.0 HINTS ${_GLIB_LIBRARY_DIR})
set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GMODULE_LIBRARIES)
elseif (${_component} STREQUAL "gthread")
find_library(GLIB_GTHREAD_LIBRARIES NAMES gthread-2.0 HINTS ${_GLIB_LIBRARY_DIR})
set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GTHREAD_LIBRARIES)
elseif (${_component} STREQUAL "gio-unix")
# gio-unix is compiled as part of the gio library, but the include paths
# are separate from the shared glib ones. Since this is currently only used
# by WebKitGTK+ we don't go to extraordinary measures beyond pkg-config.
pkg_check_modules(GIO_UNIX QUIET gio-unix-2.0)
endif ()
endforeach ()
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(GLIB REQUIRED_VARS GLIB_INCLUDE_DIRS GLIB_LIBRARIES ${ADDITIONAL_REQUIRED_VARS}
VERSION_VAR GLIB_VERSION)
mark_as_advanced(
GLIBCONFIG_INCLUDE_DIR
GLIB_GIO_LIBRARIES
GLIB_GIO_UNIX_LIBRARIES
GLIB_GMODULE_LIBRARIES
GLIB_GOBJECT_LIBRARIES
GLIB_GTHREAD_LIBRARIES
GLIB_INCLUDE_DIR
GLIB_INCLUDE_DIRS
GLIB_LIBRARIES
)

58
cmake/FindLibNotify.cmake Normal file
View File

@@ -0,0 +1,58 @@
# - Try to find LibNotify
# This module defines the following variables:
#
# LIBNOTIFY_FOUND - LibNotify was found
# LIBNOTIFY_INCLUDE_DIRS - the LibNotify include directories
# LIBNOTIFY_LIBRARIES - link these to use LibNotify
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
# Copyright (C) 2014 Collabora Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Source from:
# https://sources.debian.org/src/qtwebkit-opensource-src/5.212.0~alpha2-21/Source/cmake/FindLibNotify.cmake/
find_package(PkgConfig)
pkg_check_modules(LIBNOTIFY QUIET libnotify)
find_path(LIBNOTIFY_INCLUDE_DIRS
NAMES notify.h
HINTS ${LIBNOTIFY_INCLUDEDIR}
${LIBNOTIFY_INCLUDE_DIRS}
PATH_SUFFIXES libnotify
)
find_library(LIBNOTIFY_LIBRARIES
NAMES notify
HINTS ${LIBNOTIFY_LIBDIR}
${LIBNOTIFY_LIBRARY_DIRS}
)
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibNotify REQUIRED_VARS LIBNOTIFY_INCLUDE_DIRS LIBNOTIFY_LIBRARIES
VERSION_VAR LIBNOTIFY_VERSION)
mark_as_advanced(
LIBNOTIFY_INCLUDE_DIRS
LIBNOTIFY_LIBRARIES
)

51
cmake/FindXCB.cmake Normal file
View File

@@ -0,0 +1,51 @@
# - FindXCB
#
# Copyright 2015 Valve Coporation
find_package(PkgConfig)
if(NOT XCB_FIND_COMPONENTS)
set(XCB_FIND_COMPONENTS xcb)
endif()
include(FindPackageHandleStandardArgs)
set(XCB_FOUND true)
set(XCB_INCLUDE_DIRS "")
set(XCB_LIBRARIES "")
foreach(comp ${XCB_FIND_COMPONENTS})
# component name
string(TOUPPER ${comp} compname)
string(REPLACE "-" "_" compname ${compname})
# header name
string(REPLACE "xcb-" "" headername xcb/${comp}.h)
# library name
set(libname ${comp})
pkg_check_modules(PC_${comp} QUIET ${comp})
find_path(${compname}_INCLUDE_DIR NAMES ${headername}
HINTS
${PC_${comp}_INCLUDEDIR}
${PC_${comp}_INCLUDE_DIRS}
)
find_library(${compname}_LIBRARY NAMES ${libname}
HINTS
${PC_${comp}_LIBDIR}
${PC_${comp}_LIBRARY_DIRS}
)
find_package_handle_standard_args(${comp}
FOUND_VAR ${comp}_FOUND
REQUIRED_VARS ${compname}_INCLUDE_DIR ${compname}_LIBRARY)
mark_as_advanced(${compname}_INCLUDE_DIR ${compname}_LIBRARY)
list(APPEND XCB_INCLUDE_DIRS ${${compname}_INCLUDE_DIR})
list(APPEND XCB_LIBRARIES ${${compname}_LIBRARY})
if(NOT ${comp}_FOUND)
set(XCB_FOUND false)
endif()
endforeach()
list(REMOVE_DUPLICATES XCB_INCLUDE_DIRS)

335
cmake/git_watcher.cmake Normal file
View File

@@ -0,0 +1,335 @@
# git_watcher.cmake
# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake
#
# Released under the MIT License.
# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE
# This file defines a target that monitors the state of a git repo.
# If the state changes (e.g. a commit is made), then a file gets reconfigured.
# Here are the primary variables that control script behavior:
#
# PRE_CONFIGURE_FILE (REQUIRED)
# -- The path to the file that'll be configured.
#
# POST_CONFIGURE_FILE (REQUIRED)
# -- The path to the configured PRE_CONFIGURE_FILE.
#
# GIT_STATE_FILE (OPTIONAL)
# -- The path to the file used to store the previous build's git state.
# Defaults to the current binary directory.
#
# GIT_WORKING_DIR (OPTIONAL)
# -- The directory from which git commands will be run.
# Defaults to the directory with the top level CMakeLists.txt.
#
# GIT_EXECUTABLE (OPTIONAL)
# -- The path to the git executable. It'll automatically be set if the
# user doesn't supply a path.
#
# GIT_FAIL_IF_NONZERO_EXIT (optional)
# -- Raise a FATAL_ERROR if any of the git commands return a non-zero
# exit code. This is set to TRUE by default. You can set this to FALSE
# if you'd like the build to continue even if a git command fails.
#
# DESIGN
# - This script was designed similar to a Python application
# with a Main() function. I wanted to keep it compact to
# simplify "copy + paste" usage.
#
# - This script is invoked under two CMake contexts:
# 1. Configure time (when build files are created).
# 2. Build time (called via CMake -P).
# The first invocation is what registers the script to
# be executed at build time.
#
# MODIFICATIONS
# You may wish to track other git properties like when the last
# commit was made. There are two sections you need to modify,
# and they're tagged with a ">>>" header.
# Short hand for converting paths to absolute.
macro(PATH_TO_ABSOLUTE var_name)
get_filename_component(${var_name} "${${var_name}}" ABSOLUTE)
endmacro()
# Check that a required variable is set.
macro(CHECK_REQUIRED_VARIABLE var_name)
if(NOT DEFINED ${var_name})
message(FATAL_ERROR "The \"${var_name}\" variable must be defined.")
endif()
PATH_TO_ABSOLUTE(${var_name})
endmacro()
# Check that an optional variable is set, or, set it to a default value.
macro(CHECK_OPTIONAL_VARIABLE_NOPATH var_name default_value)
if(NOT DEFINED ${var_name})
set(${var_name} ${default_value})
endif()
endmacro()
# Check that an optional variable is set, or, set it to a default value.
# Also converts that path to an abspath.
macro(CHECK_OPTIONAL_VARIABLE var_name default_value)
CHECK_OPTIONAL_VARIABLE_NOPATH(${var_name} ${default_value})
PATH_TO_ABSOLUTE(${var_name})
endmacro()
CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_FILE)
CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_FILE)
CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${CMAKE_BINARY_DIR}/git-state-hash")
CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${CMAKE_SOURCE_DIR}")
CHECK_OPTIONAL_VARIABLE_NOPATH(GIT_FAIL_IF_NONZERO_EXIT TRUE)
# Check the optional git variable.
# If it's not set, we'll try to find it using the CMake packaging system.
if(NOT DEFINED GIT_EXECUTABLE)
find_package(Git QUIET REQUIRED)
endif()
CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE)
set(_state_variable_names
GIT_RETRIEVED_STATE
GIT_HEAD_SHA1
GIT_IS_DIRTY
GIT_AUTHOR_NAME
GIT_AUTHOR_EMAIL
GIT_COMMIT_DATE_ISO8601
GIT_COMMIT_SUBJECT
GIT_COMMIT_BODY
GIT_DESCRIBE
# >>>
# 1. Add the name of the additional git variable you're interested in monitoring
# to this list.
)
# Macro: RunGitCommand
# Description: short-hand macro for calling a git function. Outputs are the
# "exit_code" and "output" variables.
macro(RunGitCommand)
execute_process(COMMAND
"${GIT_EXECUTABLE}" ${ARGV}
WORKING_DIRECTORY "${_working_dir}"
RESULT_VARIABLE exit_code
OUTPUT_VARIABLE output
ERROR_VARIABLE stderr
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT exit_code EQUAL 0)
set(ENV{GIT_RETRIEVED_STATE} "false")
# Issue 26: git info not properly set
#
# Check if we should fail if any of the exit codes are non-zero.
if(GIT_FAIL_IF_NONZERO_EXIT)
string(REPLACE ";" " " args_with_spaces "${ARGV}")
message(FATAL_ERROR "${stderr} (${GIT_EXECUTABLE} ${args_with_spaces})")
endif()
endif()
endmacro()
# Function: GetGitState
# Description: gets the current state of the git repo.
# Args:
# _working_dir (in) string; the directory from which git commands will be executed.
function(GetGitState _working_dir)
# This is an error code that'll be set to FALSE if the
# RunGitCommand ever returns a non-zero exit code.
set(ENV{GIT_RETRIEVED_STATE} "true")
# Get whether or not the working tree is dirty.
RunGitCommand(status --porcelain)
if(NOT exit_code EQUAL 0)
set(ENV{GIT_IS_DIRTY} "false")
else()
if(NOT "${output}" STREQUAL "")
set(ENV{GIT_IS_DIRTY} "true")
else()
set(ENV{GIT_IS_DIRTY} "false")
endif()
endif()
# There's a long list of attributes grabbed from git show.
set(object HEAD)
RunGitCommand(show -s "--format=%H" ${object})
if(exit_code EQUAL 0)
set(ENV{GIT_HEAD_SHA1} ${output})
endif()
RunGitCommand(show -s "--format=%an" ${object})
if(exit_code EQUAL 0)
set(ENV{GIT_AUTHOR_NAME} "${output}")
endif()
RunGitCommand(show -s "--format=%ae" ${object})
if(exit_code EQUAL 0)
set(ENV{GIT_AUTHOR_EMAIL} "${output}")
endif()
RunGitCommand(show -s "--format=%ci" ${object})
if(exit_code EQUAL 0)
set(ENV{GIT_COMMIT_DATE_ISO8601} "${output}")
endif()
RunGitCommand(show -s "--format=%s" ${object})
if(exit_code EQUAL 0)
# Escape quotes
string(REPLACE "\"" "\\\"" output "${output}")
set(ENV{GIT_COMMIT_SUBJECT} "${output}")
endif()
RunGitCommand(show -s "--format=%b" ${object})
if(exit_code EQUAL 0)
if(output)
# Escape quotes
string(REPLACE "\"" "\\\"" output "${output}")
# Escape line breaks in the commit message.
string(REPLACE "\r\n" "\\r\\n\\\r\n" safe "${output}")
if(safe STREQUAL output)
# Didn't have windows lines - try unix lines.
string(REPLACE "\n" "\\n\\\n" safe "${output}")
endif()
else()
# There was no commit body - set the safe string to empty.
set(safe "")
endif()
set(ENV{GIT_COMMIT_BODY} "\"${safe}\"")
else()
set(ENV{GIT_COMMIT_BODY} "\"\"") # empty string.
endif()
# Get output of git describe
RunGitCommand(describe --tags ${object})
if(NOT exit_code EQUAL 0)
set(ENV{GIT_DESCRIBE} "unknown")
else()
set(ENV{GIT_DESCRIBE} "${output}")
endif()
# >>>
# 2. Additional git properties can be added here via the
# "execute_process()" command. Be sure to set them in
# the environment using the same variable name you added
# to the "_state_variable_names" list.
endfunction()
# Function: GitStateChangedAction
# Description: this function is executed when the state of the git
# repository changes (e.g. a commit is made).
function(GitStateChangedAction)
foreach(var_name ${_state_variable_names})
set(${var_name} $ENV{${var_name}})
endforeach()
configure_file("${PRE_CONFIGURE_FILE}" "${POST_CONFIGURE_FILE}" @ONLY)
endfunction()
# Function: HashGitState
# Description: loop through the git state variables and compute a unique hash.
# Args:
# _state (out) string; a hash computed from the current git state.
function(HashGitState _state)
set(ans "")
foreach(var_name ${_state_variable_names})
string(SHA256 ans "${ans}$ENV{${var_name}}")
endforeach()
set(${_state} ${ans} PARENT_SCOPE)
endfunction()
# Function: CheckGit
# Description: check if the git repo has changed. If so, update the state file.
# Args:
# _working_dir (in) string; the directory from which git commands will be ran.
# _state_changed (out) bool; whether or no the state of the repo has changed.
function(CheckGit _working_dir _state_changed)
# Get the current state of the repo.
GetGitState("${_working_dir}")
# Convert that state into a hash that we can compare against
# the hash stored on-disk.
HashGitState(state)
# Issue 14: post-configure file isn't being regenerated.
#
# Update the state to include the SHA256 for the pre-configure file.
# This forces the post-configure file to be regenerated if the
# pre-configure file has changed.
file(SHA256 ${PRE_CONFIGURE_FILE} preconfig_hash)
string(SHA256 state "${preconfig_hash}${state}")
# Check if the state has changed compared to the backup on disk.
if(EXISTS "${GIT_STATE_FILE}")
file(READ "${GIT_STATE_FILE}" OLD_HEAD_CONTENTS)
if(OLD_HEAD_CONTENTS STREQUAL "${state}")
# State didn't change.
set(${_state_changed} "false" PARENT_SCOPE)
return()
endif()
endif()
# The state has changed.
# We need to update the state file on disk.
# Future builds will compare their state to this file.
file(WRITE "${GIT_STATE_FILE}" "${state}")
set(${_state_changed} "true" PARENT_SCOPE)
endfunction()
# Function: SetupGitMonitoring
# Description: this function sets up custom commands that make the build system
# check the state of git before every build. If the state has
# changed, then a file is configured.
function(SetupGitMonitoring)
add_custom_target(check_git
ALL
DEPENDS ${PRE_CONFIGURE_FILE}
BYPRODUCTS
${POST_CONFIGURE_FILE}
${GIT_STATE_FILE}
COMMENT "Checking the git repository for changes..."
COMMAND
${CMAKE_COMMAND}
-D_BUILD_TIME_CHECK_GIT=TRUE
-DGIT_WORKING_DIR=${GIT_WORKING_DIR}
-DGIT_EXECUTABLE=${GIT_EXECUTABLE}
-DGIT_STATE_FILE=${GIT_STATE_FILE}
-DPRE_CONFIGURE_FILE=${PRE_CONFIGURE_FILE}
-DPOST_CONFIGURE_FILE=${POST_CONFIGURE_FILE}
-DGIT_FAIL_IF_NONZERO_EXIT=${GIT_FAIL_IF_NONZERO_EXIT}
-P "${CMAKE_CURRENT_LIST_FILE}")
endfunction()
# Function: Main
# Description: primary entry-point to the script. Functions are selected based
# on whether it's configure or build time.
function(Main)
if(_BUILD_TIME_CHECK_GIT)
# Check if the repo has changed.
# If so, run the change action.
CheckGit("${GIT_WORKING_DIR}" changed)
if(changed OR NOT EXISTS "${POST_CONFIGURE_FILE}")
GitStateChangedAction()
endif()
else()
# >> Executes at configure time.
SetupGitMonitoring()
endif()
endfunction()
# And off we go...
Main()

View File

@@ -1,42 +0,0 @@
#-------------------------------------------------
#
# Project created by QtCreator 2019-06-30T12:53:03
#
#-------------------------------------------------
QT += core gui webkitwidgets network concurrent
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = deepin-community-store
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += main.cpp\
mainwindow.cpp \
widget.cpp \
downloadlist.cpp
HEADERS += mainwindow.h \
widget.h \
downloadlist.h
CONFIG += link_pkgconfig
PKGCONFIG += dtkwidget
CONFIG += c++11
FORMS += \
widget.ui \
downloadlist.ui

View File

@@ -0,0 +1,52 @@
# 项目结构和命名规范
## 文件夹结构
### cmake
主要用于 CMake 配置时需要用到的 CMake 模块脚本。
### gui
主要用于显示界面的 C++ 代码文件(即包含 Qt Widgets 代码),无论包含多少与其他组件的联系,都在这个目录内。
这包括 SpkUi 的自定义界面组件、商店主界面自身的 Page 类等等。
### inc
项目中所有以 `#include "xxx.h"` 方式包含到代码中的标头文件,全部放入此目录。
标头文件并不强求目录结构明确,文件用途或来源特殊的除外。
### src
商店主体逻辑。包含基础的 `SpkStore` 类、`main.cpp` 等核心逻辑。
新加入的逻辑如单个功能多于一个cpp文件则必须分装到一个目录内。
### plugin
适配 Deepin 以及其他平台的平台相关插件。
## 命名规范
### 通用规范
类名、成员、方法名一律采用大驼峰形式。例如,`SpkTitleBar::SetTitle` 方法。
C++ 规范要求的和第三方库有较大差异的除外。例如,`SpkUiMessage::_notify`成员。
临时变量采用小驼峰形式或全小写。
名称要对功能要有基础的描述,例如,`SpkUi::SpkCategorySelector` 类。抽象核心逻辑除外。
### 类名
如非第三方代码,一般以 `Spk` 前缀标明。
实现 UI 的类都应归于 `SpkUi` 命名空间。
## 类定义
### 一般规则
Qt 类的 `Q_OBJECT` 应置于类定义最顶端。
`public` `protected``private` 等标签中只应该包含一类成员,
如单个 `public` 标签内只能包含方法,或成员。
信号、槽等不得与普通方法混合。
<!--TODO-->

6
docs/translating.md Normal file
View File

@@ -0,0 +1,6 @@
# 翻译
由于CMake和Qt的稀烂集成我们不能使用CMake自动lupdate更新翻译。
如果需要更新翻译,请使用`make run_lupdate`目标进行。qm文件的编译无需其他操作只需build即可。

View File

@@ -1,134 +0,0 @@
#include "downloadlist.h"
#include "ui_downloadlist.h"
#include <QDebug>
#include <QIcon>
#include <QPixmap>
#include <QtConcurrent>
#include <QProcess>
bool downloadlist::isInstall=false;
downloadlist::downloadlist(QWidget *parent) :
QWidget(parent),
ui(new Ui::downloadlist)
{
ui->setupUi(this);
ui->pushButton->setEnabled(false);
ui->progressBar->setValue(0);
ui->label_filename->hide();
ui->pushButton->hide();
ui->label->setStyleSheet("color:#000000");
}
downloadlist::~downloadlist()
{
delete ui;
}
void downloadlist::setValue(long long value)
{
ui->progressBar->setValue(value);
ui->label_2->setText(QString::number((double)value/100)+"%");
if(ui->label_2->text()=="100%"){
ui->label_2->setText("已完成,等待安装");
}
}
void downloadlist::setMax(long long max)
{
ui->progressBar->setMaximum(max);
}
void downloadlist::setName(QString name)
{
ui->label->setText(name);
}
QString downloadlist::getName()
{
return ui->label_filename->text();
}
void downloadlist::readyInstall()
{
if(!close){
ui->progressBar->hide();
ui->pushButton->setEnabled(true);
ui->pushButton->show();
ui->pushButton_2->hide();
system("notify-send \""+ui->label->text().toUtf8()+"下载完成,等待安装\"" +" --icon=/tmp/deepin-community-store/icon_"+QString::number(num).toUtf8()+".png");
}
}
void downloadlist::choose(bool isChoosed)
{
if(isChoosed){
ui->label->setStyleSheet("color:#FFFFFF");
ui->label_2->setStyleSheet("color:#FFFFFF");
}else {
ui->label->setStyleSheet("color:#000000");
ui->label_2->setStyleSheet("color:#000000");
}
}
void downloadlist::setFileName(QString fileName)
{
ui->label_filename->setText(fileName);
}
void downloadlist::seticon(const QPixmap icon)
{
ui->label_3->setPixmap(icon);
}
void downloadlist::closeDownload()
{
on_pushButton_2_clicked();
}
void downloadlist::on_pushButton_clicked()
{
if(!isInstall){
isInstall=true;
ui->pushButton->setEnabled(false);
ui->label_2->setText("正在安装,请稍候");
QtConcurrent::run([=](){
QProcess installer;
installer.start("pkexec apt install -y /tmp/deepin-community-store/"+ui->label_filename->text().toUtf8());
installer.waitForFinished();
QString error=QString::fromStdString(installer.readAllStandardError().toStdString());
QStringList everyError=error.split("\n");
bool haveError=false;
bool notRoot=false;
for (int i=0;i<everyError.size();i++) {
qDebug()<<everyError[i].left(2);
if(everyError[i].left(2)=="E:"){
haveError=true;
}
if(everyError[i].right(14)=="Not authorized"){
notRoot=true;
}
}
if(!haveError && !notRoot){
ui->pushButton->hide();
ui->label_2->setText("安装完成");
}else if(haveError){
ui->pushButton->hide();
ui->label_2->setText("安装出现错误");
}else {
ui->label_2->setText("安装被终止");
}
isInstall=false;
});
// system("x-terminal-emulator -e sudo apt install -y ./"+ui->label_filename->text().toUtf8());
qDebug()<<ui->label_filename->text().toUtf8();
}
}
void downloadlist::on_pushButton_2_clicked()
{
ui->label_2->setText("已取消下载");
ui->pushButton_2->setEnabled(false);
ui->progressBar->hide();
close=true;
}

View File

@@ -1,41 +0,0 @@
#ifndef DOWNLOADLIST_H
#define DOWNLOADLIST_H
#include <QWidget>
namespace Ui {
class downloadlist;
}
class downloadlist : public QWidget
{
Q_OBJECT
public:
explicit downloadlist(QWidget *parent = nullptr);
~downloadlist();
void setValue(long long);
void setMax(long long);
void setName(QString);
QString getName();
void readyInstall();
void choose(bool);
bool free;
void setFileName(QString);
void seticon(const QPixmap);
void closeDownload();
int num;
bool close=false;
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
private:
Ui::downloadlist *ui;
static bool isInstall;
};
//bool downloadlist::isInstall=false;
#endif // DOWNLOADLIST_H

View File

@@ -1,214 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>downloadlist</class>
<widget class="QWidget" name="downloadlist">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>744</width>
<height>54</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QLabel" name="label_filename">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QWidget" name="widget" native="true">
<property name="minimumSize">
<size>
<width>350</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>10</height>
</size>
</property>
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>等待开始下载</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="6">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="7">
<widget class="QPushButton" name="pushButton">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>安装</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>13</pointsize>
</font>
</property>
<property name="text">
<string>名称</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>icon</string>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="8">
<widget class="QPushButton" name="pushButton_2">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,314 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "page/spkpageappdetails.h"
#include "spkutils.h"
#include "spkappitem.h"
namespace SpkUi
{
constexpr QSize SpkPageAppDetails::IconSize;
void SpkPageAppDetails::LoadAppResources(QString aPkgName, QString aIcon, QStringList aScreenshots,
QStringList aTags)
{
QPixmap pic;
// Load icon
auto res = RES->RequestResource(0, aPkgName, SpkResource::ResourceType::AppIcon, aIcon);
if(res.status == SpkResource::ResourceStatus::Ready)
{
if(pic.loadFromData(res.data))
mAppIcon->setPixmap(pic.scaled(IconSize,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
else
{
mAppIcon->setPixmap(mBrokenImg);
RES->PurgeCachedResource(aPkgName, SpkResource::ResourceType::AppIcon, 0);
}
}
// Load screenshots. Screenshots have id starting with 1.
if(aScreenshots.isEmpty())
return;
else
{
auto count = aScreenshots.size();
mImgViewer->SetImageTotal(count);
if(count > mScreenshotPreviews.size())
{
auto from = mScreenshotPreviews.size(), to = count - mScreenshotPreviews.size();
for(int i = 0; i <= to; i++)
{
auto wid = new SpkClickLabel;
wid->setProperty("shotId", from + i + 1);
wid->setFixedHeight(200);
wid->setCursor(Qt::PointingHandCursor);
connect(wid, &SpkClickLabel::Pressed, this, &SpkPageAppDetails::ImageClicked);
mScreenshotPreviews.append(wid);
mScreenshotLay->addWidget(wid);
}
}
}
int shotId = 0;
for(auto &i : aScreenshots)
{
shotId++;
res = RES->RequestResource(shotId, aPkgName, SpkResource::ResourceType::AppScreenshot, i,
shotId);
auto preview = mScreenshotPreviews[shotId - 1];
preview->setVisible(true);
if(res.status == SpkResource::ResourceStatus::Ready)
{
if(pic.loadFromData(res.data))
{
mAppImages[shotId] = pic;
mImgViewer->SetPixmap(shotId, &mAppImages[shotId]);
preview->setPixmap(pic.scaledToHeight(200, Qt::SmoothTransformation));
}
else
{
mAppImages[shotId] = mBrokenImg;
RES->PurgeCachedResource(aPkgName, SpkResource::ResourceType::AppScreenshot, 0);
}
}
else
{
preview->setPixmap(mIconLoading);
}
}
// TODO: tags
}
void SpkPageAppDetails::SetWebsiteLink(QString url)
{
mWebsite->setText(QString("<a href=\"%1\">%2</a>").arg(url, tr("Website link")));
}
void SpkPageAppDetails::SetPackagePath(QString url)
{
mPkgPath = url;
}
SpkPageAppDetails::SpkPageAppDetails(QWidget *parent) : SpkPageBase(parent),
mBrokenImg(QIcon(":/icons/broken-icon.svg").pixmap(SpkAppItem::IconSize_)),
mIconLoading(QIcon(":/icons/loading-icon.svg").pixmap(SpkAppItem::IconSize_))
{
mMainArea = new QScrollArea;
mMainArea->setWidgetResizable(true);
mMainArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
mMainLay = new QVBoxLayout(this);
mMainLay->addWidget(mMainArea);
mBottomBar = new QWidget;
mMainLay->addWidget(mBottomBar);
mDetailsLay = new QVBoxLayout(mMainArea);
mDetailsLay->setSizeConstraint(QLayout::SetMinAndMaxSize);
mAppIcon = new QLabel;
mAppTitle = new QLabel;
mAppTitle->setObjectName("styDetTitle");
mAppTitle->setWordWrap(true);
mAppDescription = new QLabel;
mAppDescription->setObjectName("styDetDesc");
mAppDescription->setWordWrap(true);
mAppShortDesc = new QLabel;
mAppShortDesc->setObjectName("styDetDesc");
mAppShortDesc->setWordWrap(true);
// NOTE: Seems Qt have trouble dealing with wrapped text here. Removing the following operations
// to mAppShortDesc will result in broken layout. Very possible that it's a Qt bug.
mAppShortDesc->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
mAppShortDesc->setMinimumWidth(100);
mVersion = new QLabel;
mVersion->setWordWrap(true);
mWebsite = new QLabel;
mPkgName = new QLabel;
mPkgName->setObjectName("styDetPkg");
mPkgName->setWordWrap(true);
mTitleLay = new QVBoxLayout;
mTitleLay->setAlignment(Qt::AlignTop);
mTitleLay->addWidget(mAppTitle);
mTitleLay->addWidget(mVersion);
mTitleLay->addWidget(mAppShortDesc);
mTitleLay->addWidget(mPkgName);
mTitleLay->addWidget(mWebsite);
mTitleLay->setSpacing(0);
mIconTitleLay = new QHBoxLayout;
mIconTitleLay->setAlignment(Qt::AlignLeft);
mIconTitleLay->addWidget(mAppIcon);
mIconTitleLay->addSpacing(15);
mIconTitleLay->addLayout(mTitleLay);
mIconTitleWidget = new QWidget;
mIconTitleWidget->setLayout(mIconTitleLay);
mAuthor = new SpkDetailEntry;
mAuthor->SetTitle(tr("Author"));
mContributor = new SpkDetailEntry;
mContributor->SetTitle(tr("Contributor"));
// mSite = new SpkDetailEntry;
// mSite->SetTitle(tr("Website"));
mArch = new SpkDetailEntry;
mArch->SetTitle(tr("Architecture"));
mSize = new SpkDetailEntry;
mSize->SetTitle(tr("Size"));
mDetailLay = new SpkStretchLayout;
mDetailLay->setSpacing(12);
mDetailLay->addWidget(mAuthor);
mDetailLay->addWidget(mContributor);
mDetailLay->addWidget(mSize);
mDetailLay->addWidget(mArch);
// mDetailLay->addWidget(mSite);
// mDetailWidget = new QWidget;
// mDetailWidget->setLayout(mDetailLay);
// mDetailWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
mDetailsLay->setAlignment(Qt::AlignTop);
mDetailsLay->addWidget(mIconTitleWidget);
mDetailsLay->addLayout(mDetailLay);
mDetailsLay->addWidget(mAppDescription);
// mMainLay->addStretch();
mScreenshotLay = new QHBoxLayout;
mScreenshotArea = new QScrollArea;
mWid4ShotArea = new QWidget;
mWid4ShotArea->setLayout(mScreenshotLay);
mScreenshotArea->setWidget(mWid4ShotArea);
mScreenshotArea->setWidgetResizable(true);
mScreenshotArea->setFixedHeight(230);
mScreenshotArea->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
mDetailsLay->addWidget(mScreenshotArea);
mWid4MainArea = new QWidget;
mWid4MainArea->setLayout(mDetailsLay);
mMainArea->setWidget(mWid4MainArea);
mWebsite->setTextFormat(Qt::RichText);
mWebsite->setOpenExternalLinks(true);
// Bottom bar buttons
mBottomBarLay = new QHBoxLayout;
mBottomBar->setLayout(mBottomBarLay);
mBtnDownload = new QPushButton;
mBtnDownload->setText(tr("Download"));
// mBtnInstall = new QPushButton;
// mBtnInstall->setText(tr("Install"));
mBtnUninstall = new QPushButton;
mBtnUninstall->setText(tr("Uninstall"));
mBtnRequestUpdate = new QPushButton;
mBtnRequestUpdate->setText(tr("Request Update"));
mBtnReport = new QPushButton;
mBtnReport->setText(tr("Report"));
mBottomBarLay->addStretch();
mBottomBarLay->addWidget(mBtnDownload);
// mBottomBarLay->addWidget(mBtnInstall);
mBottomBarLay->addWidget(mBtnUninstall);
mBottomBarLay->addWidget(mBtnRequestUpdate);
mBottomBarLay->addWidget(mBtnReport);
mImgViewer = new SpkImgViewer;
mImgViewer->setVisible(false);
connect(mBtnDownload, &QPushButton::clicked,
[=](){ emit RequestDownload(mAppTitle->text(), mPkgName->text(),
"/store/reading/youdao-dict/youdao-dict_6.0.0-0~ubuntu_amd64.deb");
});
}
SpkPageAppDetails::~SpkPageAppDetails()
{
delete mImgViewer;
}
void SpkPageAppDetails::ResourceAcquisitionFinished(int id, ResourceResult result)
{
QPixmap img;
// qDebug() << "PageAppDetails: Resource" << id << "acquired";
if(!id)
{
// id == 0, icon
if(result.status == SpkResource::ResourceStatus::Ready)
{
if(img.loadFromData(result.data))
mAppIcon->setPixmap(img.scaled(SpkAppItem::IconSize_,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
else
mAppIcon->setPixmap(mBrokenImg);
}
else if(result.status == SpkResource::ResourceStatus::Failed)
{
mAppIcon->setPixmap(mBrokenImg);
RES->PurgeCachedResource(mPkgName->text(), SpkResource::ResourceType::AppIcon, 0);
}
}
else
{
auto preview = mScreenshotPreviews[id - 1];
preview->setVisible(true);
if(result.status == SpkResource::ResourceStatus::Ready)
{
if(img.loadFromData(result.data))
{
mAppImages[id] = img;
mImgViewer->SetPixmap(id, &mAppImages[id]);
mScreenshotPreviews[id - 1]->setPixmap(img.scaledToHeight(200, Qt::SmoothTransformation));
}
else
{
mImgViewer->SetPixmap(id, &mBrokenImg);
mScreenshotPreviews[id - 1]->setPixmap(mBrokenImg);
}
}
else if(result.status == SpkResource::ResourceStatus::Failed)
{
mImgViewer->SetPixmap(id, &mBrokenImg);
mScreenshotPreviews[id - 1]->setPixmap(mBrokenImg);
RES->PurgeCachedResource(mPkgName->text(), SpkResource::ResourceType::AppIcon, 0);
}
}
}
void SpkPageAppDetails::Activated()
{
RES->Acquire(this, false);
for(auto &i : mScreenshotPreviews)
i->setVisible(false);
mImgViewer->Clear();
}
void SpkPageAppDetails::ImageClicked()
{
mImgViewer->ShowWithImage(sender()->property("shotId").toInt());
}
SpkDetailEntry::SpkDetailEntry(QWidget *parent) : QWidget(parent)
{
setLayout(&mLay);
mLay.addWidget(&mTitle);
mLay.addWidget(&mField);
mTitle.setAlignment(Qt::AlignLeft);
mField.setAlignment(Qt::AlignRight);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setMinimumWidth(300);
setAutoFillBackground(true);
}
}

181
gui/page/spkpageapplist.cpp Normal file
View File

@@ -0,0 +1,181 @@
#include "page/spkpageapplist.h"
#include "spkutils.h"
#include "spkuimsg.h"
namespace SpkUi
{
SpkPageAppList::SpkPageAppList(QWidget *parent) : SpkPageBase(parent)
{
mLoadingIcon = new QPixmap(QIcon(":/icons/loading-icon.svg").pixmap(SpkAppItem::IconSize_));
mBrokenIcon = new QPixmap(QIcon(":/icons/broken-icon.svg").pixmap(SpkAppItem::IconSize_));
mAppsWidget = new QWidget;
mAppsArea = new QScrollArea(this);
mMainLay = new QVBoxLayout(this);
mItemLay = new SpkStretchLayout(mAppsWidget);
mPageSwitchWidget = new QWidget;
mPageSwitchLay = new QHBoxLayout(mPageSwitchWidget);
mBtnPgUp = new QPushButton;
mBtnPgDown = new QPushButton;
mBtnGotoPage = new QPushButton;
mPageInput = new QLineEdit;
mPageValidator = new QIntValidator(mPageInput);
mPageIndicator = new QLabel;
mPageValidator->setRange(1, 99);
mPageInput->setFixedWidth(50);
mPageInput->setValidator(mPageValidator);
mBtnGotoPage->setText(tr("Goto"));
mBtnPgUp->setText(tr("Previous"));
mBtnPgDown->setText(tr("Next"));
mBtnGotoPage->setFocusPolicy(Qt::NoFocus);
mBtnPgDown->setFocusPolicy(Qt::NoFocus);
mBtnPgUp->setFocusPolicy(Qt::NoFocus);
mPageSwitchLay->addWidget(mPageIndicator);
mPageSwitchLay->addStretch();
mPageSwitchLay->addWidget(mPageInput);
mPageSwitchLay->addWidget(mBtnGotoPage);
mPageSwitchLay->addWidget(mBtnPgUp);
mPageSwitchLay->addWidget(mBtnPgDown);
mAppsArea->setWidget(mAppsWidget);
mAppsArea->setWidgetResizable(true);
mMainLay->addWidget(mAppsArea);
mMainLay->addWidget(mPageSwitchWidget);
setLayout(mMainLay);
connect(mBtnPgUp, &QPushButton::clicked, this, &SpkPageAppList::PageUp);
connect(mBtnPgDown, &QPushButton::clicked, this, &SpkPageAppList::PageDown);
connect(mBtnGotoPage, &QPushButton::clicked, this, &SpkPageAppList::GotoPage);
}
void SpkPageAppList::AddApplicationEntry(QString name, QString pkgName, QString description,
QString iconUrl, int appId)
{
auto item = new SpkAppItem(appId, this);
auto id = mAppItemList.size();
connect(item, &SpkAppItem::clicked, this, &SpkPageAppList::ApplicationClicked);
item->SetTitle(name);
item->SetDescription(description);
item->setProperty("pkg_name", pkgName);
auto iconRes = RES->RequestResource(id, pkgName, SpkResource::ResourceType::AppIcon,
iconUrl, 0);
QPixmap icon;
if(iconRes.status == SpkResource::ResourceStatus::Ready)
{
if(icon.loadFromData(iconRes.data))
item->SetIcon(icon.scaled(SpkAppItem::IconSize_,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
else
{
item->SetIcon(*mBrokenIcon);
RES->PurgeCachedResource(pkgName, SpkResource::ResourceType::AppIcon, 0);
}
}
else
item->SetIcon(*mLoadingIcon);
mAppItemList.append(item);
mItemLay->addWidget(item);
}
void SpkPageAppList::ClearAll()
{
QWidget *itm;
QLayoutItem *layitm;
while((layitm = mItemLay->takeAt(0)))
{
itm = layitm->widget();
itm->hide();
itm->deleteLater();
}
mAppItemList.clear();
mAppsArea->verticalScrollBar()->setValue(0);
}
void SpkPageAppList::ResourceAcquisitionFinished(int id, ResourceResult result)
{
QPixmap icon;
// qDebug() << "PageAppList: Resource" << id << "acquired";
auto item = mAppItemList[id];
if(result.status == SpkResource::ResourceStatus::Ready)
{
if(icon.loadFromData(result.data))
item->SetIcon(icon.scaled(SpkAppItem::IconSize_,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
else
item->SetIcon(*mBrokenIcon);
}
else if(result.status == SpkResource::ResourceStatus::Failed)
{
item->SetIcon(*mBrokenIcon);
RES->PurgeCachedResource(item->property("pkg_name").toString(),
SpkResource::ResourceType::AppIcon, 0);
}
}
void SpkPageAppList::SetPageStatus(int total, int current, int itemCount, QString &keyword)
{
mCurrentPage = current;
mKeyword = keyword;
mPageIndicator->setText(tr("Page %1 / %2, %3 apps in total")
.arg(current).arg(total).arg(itemCount));
mBtnPgUp->setDisabled(current == 1);
mBtnPgDown->setDisabled(total == current || total == 1);
mBtnGotoPage->setDisabled(total == 1);
mPageValidator->setTop(total);
}
void SpkPageAppList::DisablePageSwitchers()
{
mBtnPgDown->setDisabled(true);
mBtnPgUp->setDisabled(true);
mBtnGotoPage->setDisabled(true);
}
void SpkPageAppList::PageUp()
{
DisablePageSwitchers();
if(mKeyword.isEmpty())
emit SwitchListPage(mCategoryId, mCurrentPage - 1);
else
emit SwitchSearchPage(mKeyword, mCurrentPage - 1);
}
void SpkPageAppList::PageDown()
{
DisablePageSwitchers();
if(mKeyword.isEmpty())
emit SwitchListPage(mCategoryId, mCurrentPage + 1);
else
emit SwitchSearchPage(mKeyword, mCurrentPage + 1);
}
void SpkPageAppList::GotoPage()
{
if(mPageInput->text().isEmpty())
return SpkUiMessage::SendStoreNotification(tr("Please enter page number to go to!"));
int page = mPageInput->text().toInt();
if(page > mPageValidator->top())
return SpkUiMessage::SendStoreNotification(tr("Page %1 is not a valid page number!")
.arg(page));
DisablePageSwitchers();
if(mKeyword.isEmpty())
emit SwitchListPage(mCategoryId, page);
else
emit SwitchSearchPage(mKeyword, page);
}
void SpkPageAppList::Activated()
{
RES->Acquire(this, false);
}
}

41
gui/page/spkpagebase.cpp Normal file
View File

@@ -0,0 +1,41 @@
#include "page/spkpagebase.h"
/*
* Documentation on
* How to Add a New Page to Main Window Side Bar
*
* 1. Derive your page widget class from SpkPageBase (reference implementation
* in SpkPageSettings) and add them to CMakeLists
*
* 2. Add the ID for the page in enum SpkUi::SpkStackedPages (in spkmainwindow.h)
* and add it as a resource context if needed
*
* 3. Include the page's header file in spkmainwindow.h and add a pointer to it in
* SpkUi::SpkMainWidget
*
* 4. Add a tree item or icon button for the corresponding page inside
* SpkUi::SpkMainWidget, and initialize it in SpkUi::SpkMainWidget::SpkMainWidget.
* Take references of existing entries, and write similar code close to each other
* to make the source look nice. Don't forget to add the item to the UI.
*
* 5. Make the linkage between the page and the sidebar item at the end of
* SpkUi::SpkMainWidget::SpkMainWidget().
*
*/
SpkPageBase::SpkPageBase(QWidget *parent) : QWidget(parent)
{
}
void SpkPageBase::ResourceAcquisitionFinished(int id, ResourceResult result)
{
Q_UNUSED(id);
Q_UNUSED(result);
}
void SpkPageBase::Activated()
{
; // Do nothing
}

View File

@@ -0,0 +1,214 @@
#include "page/spkpagedownloads.h"
#include "pkgs/spkpkgmgrbase.h"
#include "spkuimsg.h"
#include "spkutils.h"
SpkUi::SpkPageDownloads::SpkPageDownloads(QWidget *parent) :
SpkPageBase(parent)
{
mMainLay = new QVBoxLayout(this);
mLayEntries = new QVBoxLayout;
mScrollWidget = new QWidget;
mScrollArea = new QScrollArea(this);
mLayEntries->setAlignment(Qt::AlignTop);
mScrollWidget->setLayout(mLayEntries);
mScrollArea->setWidget(mScrollWidget);
mScrollArea->setWidgetResizable(true);
mMainLay->addWidget(mScrollArea);
setLayout(mMainLay);
mDownloadMgr = new SpkDownloadMgr(this);
connect(mDownloadMgr, &SpkDownloadMgr::DownloadProgressed,
this, &SpkPageDownloads::DownloadProgress);
mNextDownloadId = 0;
mCurrentStatus = Idle;
connect(mDownloadMgr, &SpkDownloadMgr::DownloadStopped,
this, &SpkPageDownloads::DownloadStopped, Qt::QueuedConnection);
connect(PKG, &SpkPkgMgrBase::ReportInstallResult,
this, &SpkPageDownloads::InstallationEnded);
}
SpkUi::SpkPageDownloads::~SpkPageDownloads()
{
// TODO
}
void SpkUi::SpkPageDownloads::DownloadProgress(qint64 downloadedBytes, qint64 totalBytes, int id)
{
if(!totalBytes)
return;
if(mCurrentStatus == Waiting && totalBytes)
{
mCurrentStatus = Downloading;
mEntries[id]->SetTotalBytes(totalBytes);
mEntries[id]->SetStatus(SpkDownloadEntry::Downloading);
}
mEntries[id]->Progress(downloadedBytes);
}
void SpkUi::SpkPageDownloads::AddDownloadTask(QString name, QString pkgName, QString path)
{
// Add a new download entry into the UI
auto entry = new SpkDownloadEntry;
auto iconData = RES->CacheLookup(pkgName, SpkResource::ResourceType::AppIcon, 0);
auto id = mNextDownloadId;
QPixmap icon;
if(iconData.status != SpkResource::ResourceStatus::Ready || !icon.loadFromData(iconData.data))
icon.load(":/icons/broken-icon.svg");
entry->SetBasicInfo(name, icon, mDownloadMgr->GetDestFilePath(path));
entry->SetStatus(SpkDownloadEntry::Waiting);
entry->setProperty("entryId", id);
entry->setProperty("path", path);
mNextDownloadId++;
mEntries[id] = entry;
mLayEntries->addWidget(entry);
connect(entry, &SpkDownloadEntry::Action,
this, &SpkPageDownloads::EntryAction);
NewDownloadTask(id, path);
}
void SpkUi::SpkPageDownloads::DownloadStopped(SpkDownloadMgr::TaskResult status, int id)
{
switch(status)
{
case SpkDownloadMgr::Success:
mEntries[id]->SetStatus(SpkDownloadEntry::ToBeInstalled);
break;
case SpkDownloadMgr::FailCannotCreateFile:
mEntries[id]->SetStatus(SpkDownloadEntry::DownloadFailed,
tr("Cannot create download file. Download failed."));
break;
case SpkDownloadMgr::FailNoVaibleServer:
mEntries[id]->SetStatus(SpkDownloadEntry::DownloadFailed,
tr("Connection unstable or server failure. Download failed."));
break;
case SpkDownloadMgr::FailCancel:
mEntries[id]->SetStatus(SpkDownloadEntry::DownloadFailed,
tr("This download was cancelled."));
break;
case SpkDownloadMgr::Fail:
mEntries[id]->SetStatus(SpkDownloadEntry::DownloadFailed,
tr("Unknown error. Download failed."));
break;
}
if(status == SpkDownloadMgr::Success)
SpkUiMessage::SendDesktopNotification(
tr("App \"%1\" downloaded, and ready to install.").arg(mEntries[id]->GetTaskName()));
else if(status != SpkDownloadMgr::FailCancel)
SpkUiMessage::SendDesktopNotification(
tr("Error occurred downloading \"%1\".").arg(mEntries[id]->GetTaskName()));
// Continue next download task
if(!mWaitingDownloads.isEmpty())
{
auto nextTask = mWaitingDownloads.dequeue();
auto nextEntry = mEntries[nextTask.first];
nextEntry->SetStatus(SpkDownloadEntry::Starting);
mDownloadMgr->StartNewDownload(nextTask.second, nextTask.first);
mCurrentStatus = Waiting;
}
else
{
mCurrentStatus = Idle;
}
}
void SpkUi::SpkPageDownloads::EntryAction(SpkDownloadEntry::EntryAction act)
{
SpkDownloadEntry *entry = static_cast<SpkDownloadEntry*>(sender());
auto id = entry->property("entryId").toInt();
switch(act)
{
case SpkDownloadEntry::AbortDownload:
mDownloadMgr->CancelCurrentDownload(); // Only one task at a time so simply abort download
break;
case SpkDownloadEntry::RetryDownload:
mLayEntries->removeWidget(entry); // Move to list tail
mLayEntries->addWidget(entry);
NewDownloadTask(id, entry->property("path").toString());
entry->SetStatus(SpkDownloadEntry::Waiting);
break;
case SpkDownloadEntry::StartInstall:
switch(PKG->ExecuteInstallation(entry->GetFilePath(), id))
{
case SpkPkgMgrBase::Succeeded:
entry->SetStatus(SpkDownloadEntry::Installing);
break;
case SpkPkgMgrBase::Failed:
entry->SetStatus(SpkDownloadEntry::InstallFailed,
tr("Failed to start installation."));
break;
default: break;
}
break;
case SpkDownloadEntry::RemoveEntry:
mLayEntries->removeWidget(entry);
mEntries.remove(id);
for(auto i = mWaitingDownloads.begin(); i != mWaitingDownloads.end(); i++)
{
if(i->first == id)
{
mWaitingDownloads.erase(i);
break;
}
}
entry->setVisible(false);
entry->deleteLater();
break;
}
}
void SpkUi::SpkPageDownloads::InstallationEnded(int id,
SpkPkgMgrBase::PkgInstallResult result,
int exitCode)
{
if(result == SpkPkgMgrBase::Succeeded)
{
mEntries[id]->SetStatus(SpkDownloadEntry::Installed);
}
else
{
mEntries[id]->SetStatus(SpkDownloadEntry::InstallFailed,
tr("Install failed, exit code: %1.").arg(exitCode));
}
}
void SpkUi::SpkPageDownloads::NewDownloadTask(int id, QString downloadPath)
{
if(mCurrentStatus != Idle)
mWaitingDownloads.enqueue({ id, downloadPath }); // Queue download task for future
else
{
auto nextEntry = mEntries[id];
nextEntry->SetStatus(SpkDownloadEntry::Starting);
mCurrentStatus = Waiting;
if(!mDownloadMgr->StartNewDownload(downloadPath, id)) // Initiate a download task when idle
{
// If fails to start then try next one. Emitting this signal causes
// SpkPageDownloads::DownloadStopped to be activated and thus tries next item in queue
emit mDownloadMgr->DownloadStopped(SpkDownloadMgr::FailNoVaibleServer, id);
}
}
}

50
gui/page/spkpagehome.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include <QDesktopServices>
#include "page/spkpagehome.h"
#include "spkabout.h"
#include "gitver.h"
SpkUi::SpkPageHome::SpkPageHome(QWidget *parent) :
SpkPageBase(parent)
{
ui = new Ui::SpkHomepage;
ui->setupUi(this);
SetupUi();
}
void SpkUi::SpkPageHome::LinkActivated(QString s)
{
QDesktopServices::openUrl(QUrl(s));
}
void SpkUi::SpkPageHome::SetupUi()
{
ui->lblIcon->setPixmap(QIcon(":/icons/spark-store.svg").pixmap(QSize(128, 128)));
ui->hlayTitle->setAlignment(Qt::AlignHCenter);
ui->lblVersion->setText(ui->lblVersion->text().arg(GitVer::DescribeTags()));
ui->lblNewAnnouncement->setVisible(false);
// Click event will propagate to the main window and cause the window to move when
// mouse enters the zoom & move detection area. Disable mouse event propagation.
ui->lblAuthor->setAttribute(Qt::WA_NoMousePropagation, true);
ui->widReloadCategory->setVisible(false);
ui->widReloadCategory->setAttribute(Qt::WA_StyledBackground);
ui->lblCategoryErrIcon->setPixmap(QIcon::fromTheme("dialog-error").pixmap(QSize(32, 32)));
connect(ui->lblAuthor, &QLabel::linkActivated,
this, &SpkPageHome::LinkActivated);
connect(ui->btnSubmit, &QPushButton::clicked,
[&](){ LinkActivated("https://upload.deepinos.org"); });
connect(ui->btnFeedback, &QPushButton::clicked,
[&](){ LinkActivated("https://www.deepinos.org/t/spark-feedback"); });
connect(ui->btnDonation, &QPushButton::clicked,
[&](){ LinkActivated("https://spark.deepinos.org.cn/"); });
connect(ui->btnAbout, &QPushButton::clicked,
[&](){ SpkAbout::Show(); });
}

View File

@@ -0,0 +1,225 @@
#include <QtConcurrent/QtConcurrentRun>
#include <QMutexLocker>
#include <QFutureWatcher>
#include "spkutils.h"
#include "page/spkpagesettings.h"
#include "spkmsgbox.h"
namespace SpkUi
{
SpkPageSettings::SpkPageSettings(QWidget *parent) :
SpkPageBase(parent)
{
mMainArea = new QScrollArea();
mMainLay = new QVBoxLayout(this);
mSettingsWidget = new QWidget(this);
ui = new Ui::SpkUiSettings;
ui->setupUi(mSettingsWidget);
mMainLay->addWidget(mMainArea);
mMainArea->setWidget(mSettingsWidget);
mMainArea->setWidgetResizable(true);
CFG->BindField("url/repo", &this->mRepoListUrl,
"https://d.store.deepinos.org.cn/store/server.list");
mBytesDownloads = mBytesResource = -1;
connect(&mFwResourceClean, &QFutureWatcher<void>::finished,
this, &SpkPageSettings::CleanedResource);
connect(&mFwResourceCount, &QFutureWatcher<void>::finished,
this, &SpkPageSettings::CountFinishResource);
connect(&mFwDownloadClean, &QFutureWatcher<void>::finished,
this, &SpkPageSettings::CleanedDownload);
connect(&mFwDownloadCount, &QFutureWatcher<void>::finished,
this, &SpkPageSettings::CountFinishDownload);
connect(ui->btnViewDownloadedContent, &QPushButton::clicked,
this, &SpkPageSettings::on_btnViewDownloadedContent_clicked);
connect(ui->btnViewResourceCache, &QPushButton::clicked,
this, &SpkPageSettings::on_btnViewResourceCache_clicked);
connect(ui->btnCleanDownloadedContent, &QPushButton::clicked,
this, &SpkPageSettings::on_btnCleanDownloadedContent_clicked);
connect(ui->btnCleanResourceCache, &QPushButton::clicked,
this, &SpkPageSettings::on_btnCleanResourceCache_clicked);
SetupUi();
}
SpkPageSettings::~SpkPageSettings()
{
delete mSettingsWidget;
}
void SpkPageSettings::SetupUi()
{
ui->lblSettingsTitle->setObjectName("styConfTitle");
ui->lblCleanup->setObjectName("styConfTitle");
ui->lblAdvanced->setObjectName("styConfTitle");
connect(ui->btnSave, &QPushButton::clicked,
this, &SpkPageSettings::SaveConfiguration);
}
void SpkPageSettings::ReadConfiguration()
{
ui->spnConcurrentResDownloads->setValue(CFG->ReadField("resource/concurrent", 5).toInt());
ui->edtApiUrl->setText(CFG->ReadField("url/api", "").toString());
ui->edtResourceUrl->setText(CFG->ReadField("url/res", "").toString());
ui->edtResourceCachePath->setText(CFG->ReadField("dirs/cache", "").toString());
ui->edtDownloadPath->setText(CFG->ReadField("dirs/download", "").toString());
ui->edtDownloadServers->setPlainText(CFG->ReadField("download/servers", "").toString());
ui->edtQssPath->setText(CFG->ReadField("internal/qss_path", "").toString());
ui->edtRepoListUrl->setText(CFG->ReadField("url/repo", "").toString());
ui->cmbLightDarkTheme->setCurrentIndex(CFG->ReadField("ui/theme", 0).toInt());
}
void SpkPageSettings::SaveConfiguration()
{
CFG->SetSettings("resource/concurrent", ui->spnConcurrentResDownloads->value());
CFG->SetField("url/api", ui->edtApiUrl->text());
CFG->SetField("url/res", ui->edtResourceUrl->text());
CFG->SetSettings("dirs/cache", ui->edtResourceCachePath->text());
CFG->SetField("dirs/download", ui->edtDownloadPath->text());
CFG->SetSettings("internal/qss_path", ui->edtQssPath->text());
CFG->SetField("url/repo", ui->edtRepoListUrl->text());
if(!CFG->SetField("download/servers", ui->edtDownloadServers->toPlainText()))
SpkMsgBox::StaticExec(tr("Cannot change distribution servers.\n"
"There's probably still downloads going on."),
tr("Cannot set distribution server"),
QMessageBox::Warning);
if(!CFG->SetField("ui/theme", ui->cmbLightDarkTheme->currentIndex()))
SpkMsgBox::StaticExec(tr("Auto mode can only be used when DDE plugin is loaded.\n"
"Option change is not applied."),
tr("Cannot set theme mode"),
QMessageBox::Warning);
}
void SpkPageSettings::CountCleaning()
{
ui->lblSizeDownloadedContent->setText(tr("Counting..."));
ui->lblSizeResourceCache->setText(tr("Counting..."));
auto futureDownload = QtConcurrent::run([&]()
{
QDirIterator itr(ui->edtDownloadPath->text().replace('*', QDir::homePath()),
QDirIterator::Subdirectories);
if(mMutDownload.tryLock(0))
{
int64_t size = 0;
while(itr.hasNext())
{
QFile f(itr.next());
size += f.size();
}
mBytesDownloads = size;
mMutDownload.unlock();
}
});
auto futureResource = QtConcurrent::run([&]()
{
QDirIterator itr(ui->edtResourceCachePath->text().replace('*', QDir::homePath()),
QDirIterator::Subdirectories);
if(mMutResource.tryLock((0)))
{
int64_t size = 0;
while(itr.hasNext())
{
QFile f(itr.next());
size += f.size();
}
mBytesResource = size;
mMutResource.unlock();
}
});
mFwDownloadCount.setFuture(futureDownload);
mFwResourceCount.setFuture(futureResource);
}
void SpkPageSettings::CleanedResource()
{
ui->lblSizeResourceCache->setText(tr("Cleaned"));
}
void SpkPageSettings::CleanedDownload()
{
ui->lblSizeDownloadedContent->setText(tr("Cleaned"));
}
void SpkPageSettings::Activated()
{
ReadConfiguration();
CountCleaning();
}
void SpkPageSettings::CountFinishResource()
{
if(mBytesResource >= 0)
ui->lblSizeResourceCache->setText(SpkUtils::BytesToSize(mBytesResource));
}
void SpkPageSettings::CountFinishDownload()
{
if(mBytesDownloads >= 0)
ui->lblSizeDownloadedContent->setText(SpkUtils::BytesToSize(mBytesDownloads));
}
void SpkPageSettings::on_btnViewResourceCache_clicked()
{
QDesktopServices::openUrl(ui->edtResourceCachePath->text().replace('*', QDir::homePath()));
}
void SpkPageSettings::on_btnViewDownloadedContent_clicked()
{
QDesktopServices::openUrl(ui->edtDownloadPath->text().replace('*', QDir::homePath()));
}
void SpkPageSettings::on_btnCleanResourceCache_clicked()
{
ui->lblSizeResourceCache->setText(tr("Cleaning..."));
auto future = QtConcurrent::run([&]()
{
QDirIterator itr(ui->edtResourceCachePath->text().replace('*', QDir::homePath()),
QDirIterator::Subdirectories);
if(mMutDownload.tryLock(0))
{
int64_t size = 0;
while(itr.hasNext())
{
QFile f(itr.next());
f.remove();
}
mBytesDownloads = size;
mMutDownload.unlock();
}
});
mFwResourceClean.setFuture(future);
}
void SpkPageSettings::on_btnCleanDownloadedContent_clicked()
{
ui->lblSizeDownloadedContent->setText(tr("Cleaning..."));
auto futureDownload = QtConcurrent::run([&]()
{
QDirIterator itr(ui->edtDownloadPath->text().replace('*', QDir::homePath()),
QDirIterator::Subdirectories);
if(mMutDownload.tryLock(0))
{
int64_t size = 0;
while(itr.hasNext())
{
QFile f(itr.next());
f.remove();
}
mBytesDownloads = size;
mMutDownload.unlock();
}
});
mFwDownloadClean.setFuture(futureDownload);
}
}

165
gui/page/spkpageuitest.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include <QApplication>
#include "spkabout.h"
#include "inc/page/spkpageuitest.h"
#include "spkpopup.h"
#include "spkui_general.h"
#include "pkgs/spkpkgmgrbase.h"
SpkUi::SpkPageUiTest::SpkPageUiTest(QWidget *parent) : QSplitter(parent)
{
setObjectName("spk_pg_qsstest");
TextStylesheet = new QTextEdit(this);
TextStylesheet->setObjectName("spk_pg_qsstest_qsstext");
TextStylesheet->setPlainText(SpkUi::CurrentStylesheet);
BtnApply = new QPushButton(this);
BtnApply->setObjectName("spk_pg_qsstest_btnapply");
BtnApply->setText("Apply");
connect(BtnApply, &QPushButton::pressed, this, &SpkPageUiTest::SetStylesheet);
BtnFetch = new QPushButton(this);
BtnFetch->setObjectName("spk_pg_qsstest_btnfetch");
BtnFetch->setText("Fetch Stylesheet");
connect(BtnFetch, &QPushButton::pressed, this, &SpkPageUiTest::FetchStylesheet);
HLayInputBtns = new QHBoxLayout;
HLayInputBtns->setObjectName("spk_pg_qsstest_hlay_inputbtns");
HLayInputBtns->addWidget(BtnFetch);
HLayInputBtns->addWidget(BtnApply);
Btn = new QPushButton(this);
Btn->setObjectName("spk_pg_qsstest_button");
Btn->setText("TestButton");
Chk = new QCheckBox(this);
Chk->setObjectName("spk_pg_qsstest_checkbox");
Chk->setText("CheckBox");
Rad = new QRadioButton(this);
Rad->setObjectName("spk_pg_qsstest_radiobtn");
Rad->setText("RadioButton");
Loading = new SpkLoading(this);
Loading->setObjectName("spk_pg_qsstest_loading");
Loading->start();
Prog = new QProgressBar(this);
Prog->setObjectName("spk_pg_qsstest_prog");
Prog->setValue(65);
Prog->setRange(0, 100);
AppItem = new SpkAppItem(0, this);
AppItem->setObjectName("spk_pg_qsstest_appitem");
AppItem->SetTitle("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
AppItem->SetDescription("Nam vehicula lacus vitae leo fermentum efficitur. "
"Phasellus finibus risus id aliquam pulvinar.");
AppItem->SetIcon(QIcon::fromTheme("dialog-information").pixmap(72, 72));
Detail1 = new SpkDetailEntry; Detail1->SetTitle("Foo"); Detail1->SetValue("1");
Detail2 = new SpkDetailEntry; Detail2->SetTitle("Foo"); Detail2->SetValue("1");
Detail3 = new SpkDetailEntry; Detail3->SetTitle("Foo"); Detail3->SetValue("1");
DetailsLay = new SpkStretchLayout;
DetailsLay->addWidget(Detail1);
DetailsLay->addWidget(Detail2);
DetailsLay->addWidget(Detail3);
DetailsWidget = new QWidget;
DetailsWidget->setLayout(DetailsLay);
PopupText = new QLineEdit(this);
PopupText->setObjectName("spk_pg_qsstest_poptext");
PopupText->setText("Hello, world");
ShowPopup = new QPushButton(this);
ShowPopup->setText("Show Popup");
connect(ShowPopup, &QPushButton::clicked, this, &SpkPageUiTest::ShowPopupSlot);
ShowAbout = new QPushButton(this);
ShowAbout->setText("Show About Dialog");
connect(ShowAbout, &QPushButton::clicked, [](){ SpkAbout::Show(); });
ShowPkgmgr = new QPushButton(this);
ShowPkgmgr->setText("Show Install Menu");
connect(ShowPkgmgr, &QPushButton::clicked, [=](){ SpkPkgMgrBase::Instance()->ExecuteInstallation(PopupText->text(), 0); });
SlideV = new QSlider(this);
SlideV->setObjectName("spk_pg_qsstest_slider_v");
SlideV->setOrientation(Qt::Vertical);
SlideV->setMaximum(1000);
SlideV->setMinimum(0);
SlideH = new QSlider(this);
SlideH->setObjectName("spk_pg_qsstest_slider_h");
SlideH->setOrientation(Qt::Horizontal);
SlideH->setMaximum(1000);
SlideH->setMinimum(0);
IconBtn = new SpkIconButton(this);
IconBtn->setObjectName("spk_pg_qsstest_iconbtn");
IconBtn->SetIcon(QIcon(":/icons/settings.svg"), QSize{ 16, 16 });
VLayTestWidgets = new QVBoxLayout;
VLayTestWidgets->setObjectName("spk_pg_qsstest_vlay_btn");
VLayTestWidgets->addWidget(Btn);
VLayTestWidgets->addWidget(Chk);
VLayTestWidgets->addWidget(Rad);
VLayTestWidgets->addWidget(IconBtn);
VLayTestWidgets->addWidget(Loading);
VLayTestWidgets->addWidget(PopupText);
VLayTestWidgets->addWidget(ShowPopup);
VLayTestWidgets->addWidget(ShowAbout);
VLayTestWidgets->addWidget(ShowPkgmgr);
VLayTestWidgets->addWidget(AppItem);
VLayTestWidgets->addWidget(DetailsWidget);
Group = new QGroupBox(this);
Group->setObjectName("spk_pg_qsstest_groupbox");
Group->setTitle("GroupBox");
Group->setLayout(VLayTestWidgets);
VLayInput = new QVBoxLayout;
VLayInput->setObjectName("spk_pg_qsstest_inputlay");
VLayInput->addWidget(TextStylesheet);
VLayInput->addLayout(HLayInputBtns);
VLayWidgets = new QVBoxLayout;
VLayWidgets->setObjectName("spk_pg_qsstest_widgetlay");
VLayWidgets->addWidget(Group);
VLayWidgets->addWidget(SlideH);
VLayWidgets->addWidget(Prog);
HLay4Slider = new QHBoxLayout;
HLay4Slider->setObjectName("spk_pg_qsstest_hlay_for_slider");
HLay4Slider->addLayout(VLayWidgets);
HLay4Slider->addWidget(SlideV);
WidL = new QWidget(this);
WidL->setObjectName("spk_pg_qsstest_widleft");
WidL->setLayout(HLay4Slider);
WidL->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
WidR = new QWidget(this);
WidR->setObjectName("spk_pg_qsstest_widright");
WidR->setLayout(VLayInput);
addWidget(WidL);
addWidget(WidR);
}
void SpkUi::SpkPageUiTest::SetStylesheet()
{
qApp->setStyleSheet(TextStylesheet->toPlainText());
}
void SpkUi::SpkPageUiTest::FetchStylesheet()
{
TextStylesheet->setPlainText(SpkUi::CurrentStylesheet);
}
void SpkUi::SpkPageUiTest::ShowPopupSlot()
{
SpkUi::Popup->Show(PopupText->text());
}

361
gui/page/ui/homepage.ui Normal file
View File

@@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SpkHomepage</class>
<widget class="QWidget" name="SpkHomepage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>648</width>
<height>554</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="hlayTitle">
<item>
<widget class="QLabel" name="lblIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>{spark-store-icon}</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblTitle">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>40</pointsize>
</font>
</property>
<property name="text">
<string>Spark Store</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblAuthor">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Brought to you by &lt;a href=&quot;https://spark-app.store&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#007af4;&quot;&gt;Spark Project&lt;/span&gt;&lt;/a&gt;, an open source software project.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>50</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QGridLayout" name="vlayBtns">
<item row="1" column="1">
<widget class="SpkNotifyDot" name="lblNewAnnouncement">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblAboutProject">
<property name="text">
<string>About this project...</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="btnFeedback">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Feedback</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="btnDonation">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Donation</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="btnAnnouncements">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Announcements</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="btnSubmit">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Submit Software</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="widReloadCategory" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="lblCategoryErrIcon">
<property name="text">
<string>{error-icon}</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="lblCategoryErr">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Category failed to load. Click the button to retry.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnReloadCategory">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Reload</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="lblVersion">
<property name="text">
<string>Version %1</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnAbout">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>About</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SpkNotifyDot</class>
<extends>QLabel</extends>
<header>spknotifydot.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

472
gui/page/ui/settings.ui Normal file
View File

@@ -0,0 +1,472 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SpkUiSettings</class>
<widget class="QWidget" name="SpkUiSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>581</width>
<height>896</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="layTitle">
<item>
<widget class="QLabel" name="lblSettingsTitle">
<property name="text">
<string>Spark Store Settings</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnSave">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="lineTitle">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblRestartHint">
<property name="text">
<string>Configuration entries marked &quot;*&quot; will only take effect after restarting Spark Store.</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="laySettings">
<item row="5" column="0">
<widget class="QLabel" name="lblApiUrl">
<property name="text">
<string>Store API URL</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="lblResourceUrl">
<property name="text">
<string>Store resource URL</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="edtDownloadPath">
<property name="spkcfg_key" stdset="0">
<string notr="true">dirs/download</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lblResourceCachePath">
<property name="text">
<string>Resource cache path*</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblAptRepo">
<property name="text">
<string>APT Repository</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="edtResourceUrl">
<property name="spkcfg_key" stdset="0">
<string>url/res</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="layAptRepo">
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QComboBox" name="cmbAptRepo">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="spkcfg_key" stdset="0">
<string notr="true">pkgmgr/apt_repo</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnFetchAptRepo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Fetch all</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnApplyAptRepo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblLightDarkTheme">
<property name="text">
<string>Light/dark theme</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="edtResourceCachePath">
<property name="spkcfg_key" stdset="0">
<string>dirs/cache</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPlainTextEdit" name="edtDownloadServers">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>80</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<property name="baseSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
<property name="spkcfg_key" stdset="0">
<string notr="true">download/servers</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cmbLightDarkTheme">
<property name="currentIndex">
<number>3</number>
</property>
<property name="frame">
<bool>true</bool>
</property>
<property name="spkcfg_key" stdset="0">
<string notr="true">gui/theme</string>
</property>
<item>
<property name="text">
<string>Auto (DDE only)</string>
</property>
</item>
<item>
<property name="text">
<string>Always Light</string>
</property>
</item>
<item>
<property name="text">
<string>Always Dark</string>
</property>
</item>
<item>
<property name="text">
<string>Manual</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblDownloadPath">
<property name="toolTip">
<string/>
</property>
<property name="toolTipDuration">
<number>-1</number>
</property>
<property name="text">
<string>Download path*</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblDownloadServers">
<property name="toolTip">
<string>Server addresses are separated with two semicolons (;;).</string>
</property>
<property name="toolTipDuration">
<number>1</number>
</property>
<property name="text">
<string>Download servers</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="edtApiUrl">
<property name="spkcfg_key" stdset="0">
<string>url/api</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="lblRepoListUrl">
<property name="text">
<string>APT Repository Source</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="edtRepoListUrl"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblHomeNotice">
<property name="text">
<string>Note: character &quot;*&quot; in paths are replaced with the current user's home path.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblCleanup">
<property name="text">
<string>Cache and Downloads cleanup</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblCleanupDescription">
<property name="text">
<string>Spark Store caches resources such as app icons and screenshots locally. If you want to free up space, you can clear them here or delete them manually.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="layCleanup">
<item row="1" column="3">
<widget class="QPushButton" name="btnCleanDownloadedContent">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="btnViewResourceCache">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>View</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnViewDownloadedContent">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>View</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblDownloadedContent">
<property name="text">
<string>Downloaded content</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblResourceCache">
<property name="text">
<string>Resource cache</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="btnCleanResourceCache">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="lblSizeResourceCache">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="lblSizeDownloadedContent">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblAdvanced">
<property name="text">
<string>Advanced</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblAdvancedDescription">
<property name="text">
<string>Advanced settings are low-level configurations that can affect usability and are not meant to be modified by average users.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="layAdvanced">
<item row="0" column="0">
<widget class="QLabel" name="lblConcurrentResDownloads">
<property name="text">
<string>Concurrent resource downloads*</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spnConcurrentResDownloads">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>6</number>
</property>
<property name="value">
<number>5</number>
</property>
<property name="spkcfg_key" stdset="0">
<string>resource/concurrent</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblQssPath">
<property name="text">
<string>Default Qt Style Sheet template*</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="edtQssPath">
<property name="spkcfg_key" stdset="0">
<string>internal/qss_path</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

133
gui/qt/elidedlabel.cpp Normal file
View File

@@ -0,0 +1,133 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
/*
* Slight modifications has been done to the code to make it fit into the project.
*/
#include "qt/elidedlabel.h"
#include <QPainter>
#include <QSizePolicy>
#include <QTextLayout>
//! [0]
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent)
: QFrame(parent)
, elided(false)
, content(text)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
}
//! [0]
//! [1]
void ElidedLabel::setText(const QString &newText)
{
content = newText;
update();
}
//! [1]
//! [2]
void ElidedLabel::paintEvent(QPaintEvent *event)
{
QFrame::paintEvent(event);
QPainter painter(this);
QFontMetrics fontMetrics = painter.fontMetrics();
bool didElide = false;
int lineSpacing = fontMetrics.lineSpacing();
int y = 0;
QTextLayout textLayout(content, painter.font());
textLayout.beginLayout();
forever {
QTextLine line = textLayout.createLine();
if (!line.isValid())
break;
line.setLineWidth(width());
int nextLineY = y + lineSpacing;
if (height() >= nextLineY + lineSpacing) {
line.draw(&painter, QPoint(0, y));
y = nextLineY;
//! [2]
//! [3]
} else {
QString lastLine = content.mid(line.textStart());
QString elidedLastLine = fontMetrics.elidedText(lastLine, Qt::ElideRight, width());
painter.drawText(QPoint(0, y + fontMetrics.ascent()), elidedLastLine);
line = textLayout.createLine();
didElide = line.isValid();
break;
}
}
textLayout.endLayout();
//! [3]
//! [4]
if (didElide != elided) {
elided = didElide;
emit elisionChanged(didElide);
}
}
ElidedLabel::ElidedLabel(QWidget *parent)
: QFrame(parent)
, elided(false)
, content("")
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
}
//! [4]

73
gui/spkabout.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include <QDesktopServices>
#include "spkabout.h"
#include "gitver.h"
SpkAbout::SpkAbout(QWidget *parent) : SpkDialog(parent)
{
setWindowModality(Qt::ApplicationModal);
// mDialogWidget->setMaximumWidth(600);
mDialogWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setFixedSize(550, 450);
SetResizable(false); // Do you like the dilemma of using self created widget?
mIconLay = new QHBoxLayout;
mSpkVersion = new QLabel;
mSpkVersion->setText(tr("<h1>Spark Store</h1>"
"<h3>Version <a href=\"https://www.spark-app.store\">%1</a></h3>"
"<t>Built on %2 %3</t>")
.arg(GitVer::DescribeTags(),
__DATE__, __TIME__));
connect(mSpkVersion, &QLabel::linkActivated,
[](const QString &link){ QDesktopServices::openUrl(link); });
mSpkIcon = new QLabel;
mSpkIcon->setPixmap(QIcon(":/icons/spark-store.svg").pixmap(QSize(128, 128)));
auto description = tr(
"Spark Store was started when Chinese home-grown Linux operating systems "
"had initially hit the market. Because the Linux desktop ecosystem is not "
"good enough at the time being, volunteers built this small App Store in "
"the hope that users can get useful applications faster.\n\n"
"Right now we are not just a Chinese group. We are discovering our way into "
"more Debian-based Linux OSes, and build a real community software repository "
"for users around the world.");
mDescriptionText = new QLabel;
mDescriptionText->setObjectName("spk_about_desc");
mDescriptionText->setWordWrap(true);
mDescriptionText->setText(description);
mIconLay->addStretch(3);
mIconLay->addWidget(mSpkIcon);
mIconLay->addStretch(1);
mIconLay->addWidget(mSpkVersion);
mIconLay->addStretch(3);
mIconLay->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
AddStretch();
AddLayout(mIconLay);
AddSpacing(18);
AddWidget(mDescriptionText);
AddStretch();
SetMargin(18, 0, 18, 18);
GetTitleBar()->SetOperationButton(SpkTitleBar::OperationButton::Close);
GetTitleBar()->SetTitle(tr("About Spark Store"));
}
SpkAbout::~SpkAbout()
{
// delete mIconLay;
// delete mDescriptionText;
}
void SpkAbout::Show()
{
SpkAbout *b = new SpkAbout;
b->Exec();
delete b;
}

63
gui/spkappitem.cpp Normal file
View File

@@ -0,0 +1,63 @@
#include <QPainter>
#include <QStyleOption>
#include "spkappitem.h"
#include "qt/elidedlabel.h"
const QSize SpkAppItem::IconSize_;
SpkAppItem::SpkAppItem(int appId, QWidget *parent) : QWidget(parent)
{
mAppId = appId;
mMainLay = new QHBoxLayout(this);
mLayText = new QVBoxLayout;
mIcon = new QLabel;
mIcon->setFixedSize(IconSize, IconSize);
mIcon->setAutoFillBackground(false);
// NOTE: Make mTitle ElidedTitle too?
mTitle = new QLabel;
mTitle->setWordWrap(false);
mTitle->setObjectName("styAppItmTitle");
mTitle->setAutoFillBackground(true);
mDescription = new ElidedLabel;
// mDescription->setWordWrap(true); // Commented out since ElidedLabel lacks of these methods
mDescription->setObjectName("styAppItmDesc");
mDescription->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// mDescription->setAlignment(Qt::AlignTop | Qt::AlignLeft);
// mDescription->setAutoFillBackground(true);
mLayText->addWidget(mTitle);
mLayText->addWidget(mDescription);
mLayText->setAlignment(Qt::AlignTop);
mMainLay->setMargin(5);
mMainLay->addWidget(mIcon);
mMainLay->addLayout(mLayText);
setMinimumHeight(82);
setMaximumHeight(82);
setMinimumWidth(300);
}
void SpkAppItem::paintEvent(QPaintEvent *e)
{
Q_UNUSED(e)
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
void SpkAppItem::mousePressEvent(QMouseEvent *e)
{
mPressCond = true;
}
void SpkAppItem::mouseReleaseEvent(QMouseEvent *e)
{
if(mPressCond)
emit clicked(mAppId);
mPressCond = false;
}

View File

@@ -0,0 +1,61 @@
#include "spkmainwindow.h"
namespace SpkUi
{
SpkCategorySelector::SpkCategorySelector(QWidget *parent) : QWidget(parent)
{
mBtnLayout = new QVBoxLayout(this);
mGroup = new QButtonGroup(this);
}
void SpkCategorySelector::AddButton(QString aBtnText, int aCategoryId, QPixmap *aBtnIcon)
{
auto btn = new SpkCategoryButton(this);
btn->SetText(aBtnText);
if(aBtnIcon)
btn->SetIcon(*aBtnIcon);
mBtnList.append(btn);
mGroup->addButton(btn, aCategoryId ? aCategoryId : -1);
mBtnLayout->addWidget(btn);
}
void SpkCategorySelector::DeleteAllButtons() // TODO: UNTESTED
{
foreach (auto i, mBtnList)
{
mBtnLayout->removeWidget(i);
mGroup->removeButton(i);
i->deleteLater();
}
mBtnList.clear();
}
SpkCategoryButton::SpkCategoryButton(QWidget *parent) : QPushButton(parent)
{
mIcon = new QLabel(this);
mIcon->setObjectName("spk_categorybtn_label");
mText = new QLabel(this);
mText->setObjectName("spk_categorybtn_text");
mLayout = new QHBoxLayout;
mLayout->setObjectName("spk_categorybtn_lay");
mLayout->addSpacing(Spacing);
mLayout->addWidget(mIcon);
mLayout->addSpacing(Spacing);
mLayout->addStretch();
mLayout->addWidget(mText);
mLayout->addStretch();
mLayout->addSpacing(Spacing);
setLayout(mLayout);
}
void SpkCategoryButton::SetIcon(QPixmap aImage)
{
mIcon->setPixmap(aImage);
}
void SpkCategoryButton::SetText(QString aText)
{
mText->setText(aText);
}
}

115
gui/spkdialog.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "spkdialog.h"
#include <QEventLoop>
SpkDialog::SpkDialog(QWidget *parent) : SpkWindow(parent)
{
mDialogWidget = new QWidget;
mMainVLay = new QVBoxLayout;
mWidgetsVLay = new QVBoxLayout();
mBtnLay = new QHBoxLayout();
mBtnGroup = new QButtonGroup(this);
mMainVLay->addLayout(mWidgetsVLay);
mMainVLay->addLayout(mBtnLay);
mBtnLay->setAlignment(Qt::AlignCenter);
SetCentralWidget(mDialogWidget);
mDialogWidget->setLayout(mMainVLay);
// idClicked is not available on platforms we support, shouldn't change it
connect(mBtnGroup, QOverload<int>::of(&QButtonGroup::buttonClicked),
this, &SpkDialog::ButtonPressed);
connect(this, &SpkWindow::Closed, this, &SpkDialog::ForceClose);
}
SpkDialog::~SpkDialog()
{
auto itp = mParentsList.begin();
for(auto itw = mWidgetsList.begin(); itw != mWidgetsList.end(); itw++)
{
(*itw)->setParent(*(itp++));// We shall never take the ownership of these widgets
}
delete mDialogWidget;
}
void SpkDialog::AddButton(QString text, SpkUi::SpkButtonStyle style)
{
auto b = new QPushButton();
b->setText(text);
b->setMinimumWidth(100);
b->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
switch(style)
{
case SpkUi::SpkButtonStyle::Recommend:
b->setObjectName("sty_recommendbtn");
break;
case SpkUi::SpkButtonStyle::Warn:
b->setObjectName("sty_warnbtn");
break;
default:
break;
}
mBtnLay->addWidget(b);
mBtnGroup->addButton(b);
}
void SpkDialog::AddWidget(QWidget *w)
{
// Adding a widget does not take the ownership.
mWidgetsVLay->addWidget(w);
mWidgetsList << w;
mParentsList << w->parentWidget();
}
void SpkDialog::AddLayout(QLayout *w)
{
mWidgetsVLay->addLayout(w);
mWidgetsList << w;
mParentsList << w->parentWidget();
}
void SpkDialog::AddSpacing(int a)
{
mWidgetsVLay->addSpacing(a);
}
void SpkDialog::AddStretch(int a)
{
mWidgetsVLay->addStretch(a);
}
void SpkDialog::SetMargin(int a)
{
mWidgetsVLay->setMargin(a);
}
int SpkDialog::Exec()
{
QEventLoop loop;
connect(this, &SpkDialog::ExitEventLoop, &loop, &QEventLoop::exit);
connect(this, &SpkDialog::CloseWindow, this, &QMainWindow::close);
show();
return loop.exec();
}
void SpkDialog::ButtonPressed(int aBtnId)
{
disconnect(this, &SpkWindow::Closed, this, &SpkDialog::ForceClose);
emit ExitEventLoop(-aBtnId - 2);
emit CloseWindow();
}
void SpkDialog::ForceClose()
{
disconnect(this, &SpkDialog::CloseWindow, this, &QMainWindow::close);
emit ExitEventLoop(-1);
}
void SpkDialog::SetMargin(int left, int top, int right, int bottom)
{
mWidgetsVLay->setContentsMargins(left, top, right, bottom);
}

201
gui/spkdownloadentry.cpp Normal file
View File

@@ -0,0 +1,201 @@

#include "spkdownloadentry.h"
#include "spklogging.h"
#include "spkutils.h"
#include <QDebug>
constexpr QSize SpkDownloadEntry::IconSize;
SpkDownloadEntry::SpkDownloadEntry(QWidget *parent)
{
mIcon = new QLabel;
mAppName = new ElidedLabel;
mMessage = new QLabel;
mProgress = new QProgressBar;
mLoading = new SpkLoading;
mBtnDelete = new QPushButton;
mBtnActions = new QPushButton;
mLayInfo = new QVBoxLayout;
mLayMsgs = new QHBoxLayout;
mLayMain = new QHBoxLayout;
mLoading->setVisible(false);
mIcon->setFixedSize(IconSize);
mProgress->setRange(0, 1000);
mLayMsgs->addWidget(mAppName);
mLayMsgs->addStretch();
mLayMsgs->addWidget(mMessage);
mLayInfo->addLayout(mLayMsgs);
mLayInfo->addWidget(mProgress);
mLayInfo->setAlignment(Qt::AlignVCenter);
mLayMain->addWidget(mIcon);
mLayMain->addLayout(mLayInfo);
mLayMain->addWidget(mLoading);
mLayMain->addWidget(mBtnActions);
mLayMain->addWidget(mBtnDelete);
setLayout(mLayMain);
connect(mBtnActions, &QPushButton::clicked, this, &SpkDownloadEntry::ActionButton);
connect(mBtnDelete, &QPushButton::clicked, this, &SpkDownloadEntry::DeleteButton);
mStatus = Invalid;
mLastReportTime = QTime::currentTime();
}
SpkDownloadEntry::~SpkDownloadEntry()
{
// TODO
}
void SpkDownloadEntry::SetTotalBytes(qint64 total)
{
mTotalBytes = total;
mReadableTotalSize = SpkUtils::BytesToSize(total);
mLastReportTime = QTime::currentTime();
}
void SpkDownloadEntry::SetBasicInfo(QString name, QPixmap icon, QString filePath)
{
mAppName->setText(name);
mIcon->setPixmap(icon.scaled(IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
mFilePath = filePath;
}
void SpkDownloadEntry::SetStatus(DownloadEntryStatus status, QString msg)
{
mStatus = status;
switch(status)
{
case Waiting:
mMessage->setText(tr("Waiting for download"));
mProgress->setVisible(false);
mBtnActions->setVisible(false);
mBtnDelete->setVisible(true);
mBtnDelete->setText(tr("Cancel"));
break;
case Starting:
mMessage->setText(tr("Starting download"));
mBtnDelete->setVisible(false);
break;
case Downloading:
mMessage->setText(tr(""));
mProgress->setVisible(true);
mBtnActions->setVisible(false);
mBtnDelete->setVisible(true);
break;
case DownloadFailed:
mMessage->setText(msg);
mProgress->setVisible(false);
mBtnActions->setVisible(true);
mBtnActions->setText(tr("Retry"));
break;
case ToBeInstalled:
mMessage->setText(tr("Download Finished"));
mProgress->setVisible(false);
mBtnActions->setVisible(true);
mBtnDelete->setVisible(false);
mBtnActions->setText(tr("Install"));
break;
case Installing:
mMessage->setText("");
mProgress->setVisible(false);
mBtnActions->setVisible(false);
mBtnDelete->setVisible(false);
mLoading->setVisible(true);
mLoading->Begin();
break;
case Installed:
mMessage->setText(tr("Installed"));
mLoading->End();
mLoading->setVisible(false);
mBtnDelete->setVisible(true);
mBtnDelete->setText(tr("Delete"));
break;
case InstallFailed:
mMessage->setText(msg.isEmpty() ? tr("Install Failed") : msg);
mLoading->End();
mLoading->setVisible(false);
mBtnActions->setVisible(true);
mBtnActions->setText(tr("Install"));
mBtnDelete->setVisible(true);
mBtnDelete->setText(tr("Cancel"));
break;
case Invalid:
break;
}
}
void SpkDownloadEntry::Progress(qint64 bytes)
{
auto now = QTime::currentTime();
auto msecDiff = mLastReportTime.msecsTo(now);
if(msecDiff != 0)
{
auto bytesPerSec = (bytes - mDownloadedBytes) / (msecDiff / 1000.0);
qDebug() << "Bytes" << bytes - mDownloadedBytes
<< "MsDiff" << msecDiff / 1000.0
<< "Bytes-Per-Seg" << bytesPerSec;
auto speedSize = SpkUtils::BytesToSize(static_cast<size_t>(bytesPerSec));
mMessage->setText(QString("%1/%2(%3/s)")
.arg(SpkUtils::BytesToSize(bytes), mReadableTotalSize, speedSize));
}
mDownloadedBytes = bytes;
mProgress->setValue(static_cast<int>(((double)bytes) / mTotalBytes * 1000));
mLastReportTime = now;
}
void SpkDownloadEntry::ActionButton()
{
switch(mStatus)
{
case DownloadFailed:
emit Action(RetryDownload);
break;
case ToBeInstalled:
case InstallFailed:
emit Action(StartInstall);
break;
default:
break;
}
}
void SpkDownloadEntry::DeleteButton()
{
switch(mStatus)
{
case Waiting:
case DownloadFailed:
case Installed:
case InstallFailed:
case ToBeInstalled:
emit Action(RemoveEntry);
break;
case Downloading:
emit Action(AbortDownload);
break;
default:
break;
}
}

56
gui/spkiconbutton.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include <QMargins>
#include <QPainter>
#include <QVariant>
#include <QDebug>
#include "spkiconbutton.h"
SpkIconButton::SpkIconButton(QWidget *parent) :
QPushButton(parent)
{
setFocusPolicy(Qt::NoFocus);
}
void SpkIconButton::SetIcon(QIcon i, QSize s)
{
mPmapPaintedIcon = i.pixmap(s);
setFixedSize((mPmapSize = s.grownBy(QMargins(IconMargin, IconMargin, IconMargin, IconMargin))));
}
void SpkIconButton::SetIcon(QPixmap m)
{
mPmapPaintedIcon = m;
setFixedSize((mPmapSize = m.size().grownBy(QMargins(IconMargin, IconMargin,
IconMargin, IconMargin))));
}
void SpkIconButton::SetIconSize(QSize s)
{
setFixedSize((mPmapSize = s.grownBy(QMargins(IconMargin, IconMargin, IconMargin, IconMargin))));
}
void SpkIconButton::paintEvent(QPaintEvent *e)
{
QPushButton::paintEvent(e);
// Paint the icon mask
QPainter p(this), p1(&mPmapPaintedIcon);
QBrush b(Qt::SolidPattern);
p.drawPixmap(IconMargin, IconMargin, mPmapPaintedIcon);
if(isDown() || isChecked())
{
b.setColor(SpkUi::ColorBtnMaskSelected);
}
else
{
b.setColor(SpkUi::ColorBtnMaskUnselected);
}
p1.setCompositionMode(QPainter::CompositionMode_SourceIn);
p1.fillRect(0, 0, mPmapSize.width(), mPmapSize.height(), b);
p1.end();
p.drawPixmap(IconMargin, IconMargin, mPmapPaintedIcon);
p.end();
}

120
gui/spkimgviewer.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "spktitlebar.h"
#include "spkimgviewer.h"
#include "spkui_general.h"
#include <QDebug>
#include <QFocusEvent>
#include <QGuiApplication>
#include <QScreen>
SpkImgViewer::SpkImgViewer(QWidget *parent) :
SpkWindow(parent),
mIconLoading(QIcon(":/icons/loading-icon.svg").pixmap({ 72, 72 }))
{
mImgIndict = new QLabel;
mImgIndict->setText("%1/%2");
mBtnPrev = new QPushButton;
mBtnPrev->setText("<");
mBtnNext = new QPushButton;
mBtnNext->setText(">");
auto titleBar = GetTitleBar();
titleBar->SetUseIcon(false);
titleBar->SetTitle(tr("Image Preview"));
titleBar->SetOperationButton(SpkTitleBar::OperationButton::Close);
auto lay = titleBar->GetUserSpace();
lay->setAlignment(Qt::AlignVCenter);
lay->addStretch();
lay->addWidget(mBtnPrev);
lay->addWidget(mImgIndict);
lay->addWidget(mBtnNext);
lay->addStretch();
mImgArea = new QScrollArea;
mImgArea->setWidgetResizable(true);
mImgArea->setContentsMargins(10, 10, 10, 10);
mImgShow = new ImgView;
mImgArea->setWidget(mImgShow);
auto w = new QWidget;
auto l = new QHBoxLayout;
l->setContentsMargins(10, 10, 10, 10);
l->addWidget(mImgArea);
w->setLayout(l);
SetCentralWidget(w);
connect(mBtnPrev, &QPushButton::clicked,
[&](){ if(mCurrentImg > 0) { SwitchToImage(--mCurrentImg); } });
connect(mBtnNext, &QPushButton::clicked,
[&](){ if(mCurrentImg < mTotalImg) { SwitchToImage(++mCurrentImg); } });
}
void SpkImgViewer::ShowWithImage(int idx)
{
SwitchToImage(idx);
show();
}
void SpkImgViewer::Clear()
{
mImgMap.clear();
mImgShow->SetPixmap(nullptr);
mCurrentImg = 1;
}
void SpkImgViewer::SetPixmap(int idx, QPixmap *img)
{
mImgMap[idx] = img;
if(mCurrentImg == idx)
{
mImgShow->SetPixmap(img);
}
ResizeToFitImageSize(img->size());
}
void SpkImgViewer::SwitchToImage(int idx)
{
auto img = mImgMap.value(idx, nullptr);
mCurrentImg = idx;
mImgShow->SetPixmap(img ? img : &mIconLoading);
if(img)
ResizeToFitImageSize(img->size());
if(idx == 1)
{
mBtnPrev->setEnabled(false);
mBtnNext->setEnabled(true);
}
else if(idx == mTotalImg)
{
mBtnPrev->setEnabled(true);
mBtnNext->setEnabled(false);
}
else
{
mBtnPrev->setEnabled(true);
mBtnNext->setEnabled(true);
}
mImgIndict->setText(QString("%1/%2").arg(mCurrentImg).arg(mTotalImg));
}
bool SpkImgViewer::event(QEvent *e)
{
if(e->type() == QEvent::WindowDeactivate)
close();
return SpkWindow::event(e);
}
void SpkImgViewer::ResizeToFitImageSize(QSize s)
{
auto targetSize = s;
targetSize.rheight() += SpkTitleBar::Height;
targetSize = s.grownBy(QMargins(10, 10, 10, 10));
targetSize = targetSize.boundedTo(SpkUi::PrimaryScreenSize * 0.8);
resize(targetSize);
targetSize /= 2;
auto targetPos = SpkUi::PrimaryScreenSize / 2 - targetSize;
move(targetPos.width(), targetPos.height());
}

79
gui/spkloading.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include <QPaintEvent>
#include <QPainter>
#include "spkloading.h"
#include "spkui_general.h"
SpkLoading::SpkLoading(QWidget *parent) : QFrame(parent)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
mAnimTimer = new QTimeLine(400, this);
mAnimTimer->setFrameRange(10, 30);
mAnimTimer->setEasingCurve(QEasingCurve::InCubic);
for(int i = 0; i < 5; i++)
mSizeList.append(20);
connect(mAnimTimer, &QTimeLine::frameChanged, this, &SpkLoading::timer);
connect(mAnimTimer, &QTimeLine::finished, this, &SpkLoading::loop);
}
void SpkLoading::paintEvent(QPaintEvent *e)
{
QPainter p(this);
QPen pen(Qt::NoPen);
QBrush b(SpkUi::CurrentColorSet[SpkUi::Qss::AccentColor], Qt::SolidPattern);
p.setBrush(b);
p.setPen(pen);
p.setRenderHint(QPainter::Antialiasing);
dx = width() / 2 - dh * 2;
dy = height() / 2;
double r;
for(int i = 0; i < 5; i++)
{
r = dh * mSizeList[i] / 80;
p.drawEllipse({ dx, dy }, r, r);
dx += dh;
}
e->accept();
}
void SpkLoading::resizeEvent(QResizeEvent *e)
{
// Calculate size of drawing space
if(mUserHeight != 0 && mUserHeight * 5 <= e->size().width())
{
dw = 5 * mUserHeight;
dh = mUserHeight;
return;
}
dh = e->size().height();
if(width() < dh * 5)
{
dw = e->size().width();
dh = dw / 5;
}
else
dw = dh * 5;
}
void SpkLoading::timer(int s)
{
for(int i = 4; i > 0; i--)
mSizeList[i] = mSizeList[i - 1];
mSizeList[0] = s;
update();
}
void SpkLoading::loop()
{
mAnimTimer->setDirection(mAnimTimer->direction() == QTimeLine::Forward ?
QTimeLine::Backward : QTimeLine::Forward);
mAnimTimer->start();
}
void SpkLoading::reset()
{
for(int i = 0; i < 5; i++)
mSizeList[i] = 20;
update();
}

570
gui/spkmainwindow.cpp Normal file
View File

@@ -0,0 +1,570 @@
#include <QGuiApplication>
#include <QScreen>
#include <QJsonArray>
#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(this);
Initialize();
SetCentralWidget(ui);
RefreshCategoryData();
GetTitleBar()->SetTitle("");
GetTitleBar()->SetUseIcon(true);
GetTitleBar()->SetIcon(QIcon(":/icons/spark-store.svg").pixmap({ 40, 40 }));
GetTitleBar()->setObjectName("spk_mw_titlebar");
GetTitleBar()->setAttribute(Qt::WA_StyledBackground);
auto size = QGuiApplication::primaryScreen()->size() * 0.5;
size = size.expandedTo(QSize(900, 600));
resize(size);
auto pos = QGuiApplication::primaryScreen()->size() * 0.5 - size * 0.5;
move(pos.width(), pos.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<SpkPageBase *>(ui->Pager->currentWidget());
if(tryActivate)
tryActivate->Activated();
ui->BtnBack->setVisible(page == SpkUi::SpkStackedPages::PgAppDetails);
ui->BtnBack->setEnabled(true);
}
}
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;
auto verify = SpkUtils::VerifyReplyJson(mCategoryGetReply, retval);
if(verify || !retval.isArray())
{
sErr(tr("Failed to load categories! Type=%1 Code=%2").arg(retval.type()).arg(verify));
sNotify(tr("Cannot load categories! Type: %1 Code: %2").arg(retval.type()).arg(verify));
ui->PageHome->ui->widReloadCategory->setVisible(true);
return;
}
ui->PageHome->ui->widReloadCategory->setVisible(false);
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;
setCursor(Qt::ArrowCursor);
int verify = SpkUtils::VerifyReplyJson(mCategoryAppListGetReply, retval);
if(verify || !retval.isObject())
{
sErr(tr("Failed to load app list of category! Type=%1 Code=%2").arg(retval.type()).arg(verify));
sNotify(tr("Failed to load app list of category! Type: %1 Code: %2").arg(retval.type()).arg(verify));
return;
}
SwitchToPage(SpkUi::PgAppList);
PopulateAppList(retval.toObject(), "");
}
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;
setCursor(Qt::ArrowCursor);
auto verify = SpkUtils::VerifyReplyJson(mCategoryAppListGetReply, retval);
if(verify || !retval.isObject())
{
sErr(tr("Failed to search keyword! Type=%1 Code=%2").arg(retval.type()).arg(verify));
sNotify(tr("Failed to search keyword! Type: %1 Code: %2").arg(retval.type()).arg(verify));
return;
}
SwitchToPage(SpkUi::PgAppList);
PopulateAppList(retval.toObject(), mCategoryAppListGetReply->property("keyword").toString());
}
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;
setCursor(Qt::ArrowCursor);
auto verify = SpkUtils::VerifyReplyJson(mAppDetailsGetReply, retval);
if(verify || !retval.isObject())
{
sErr(tr("Failed to open app details page! Type=%1 Code=%2").arg(retval.type()).arg(verify));
sNotify(tr("Failed to open app details page! Type: %1 Code: %2").arg(retval.type()).arg(verify));
return;
}
SwitchToPage(SpkUi::PgAppList);
PopulateAppDetails(retval.toObject());
}
void SpkMainWindow::PopulateAppDetails(QJsonObject appDetails)
{
QString pkgName, author, contributor, site, iconPath, arch, version, details, shortDesc, name,
pkgPath;
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();
if(appDetails.contains("deb_url") && appDetails.value("deb_url").isString())
pkgPath = appDetails.value("deb_url").toString();
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);
w->SetPackagePath(pkgPath);
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);
}
connect(ui->PageAppDetails, &SpkUi::SpkPageAppDetails::RequestDownload,
ui->PageDownloads, &SpkUi::SpkPageDownloads::AddDownloadTask);
connect(ui->PageHome->ui->btnReloadCategory, &QPushButton::clicked,
this, &SpkMainWindow::RefreshCategoryData);
connect(&SpkUi::SpkUiMetaObject, &SpkUi::UiMetaObject::SetThemeButtonVisible,
[=](bool visible)
{
ui->BtnDayNight->setVisible(visible);
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);
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", PgSettings);
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));
BtnBack = new SpkIconButton(this);
BtnBack->setFixedSize({ 40, 40 });
BtnBack->SetIcon(QIcon(":/icons/back.svg"), QSize(20, 20));
BtnBack->setVisible(false);
using SpkUi::SpkSidebarSelector;
CategoryWidget = new SpkSidebarTree(this);
CategoryWidget->setObjectName("styMwCateg");
CategoryWidget->setAutoFillBackground(true);
CategoryWidget->setColumnCount(1);
CategoryWidget->setHeaderHidden(true);
CategoryWidget->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection);
CategoryWidget->setFixedWidth(200);
//============ Sidebar entries BEGIN ============
HomepageItem = new QTreeWidgetItem(QStringList(tr("Home")));
HomepageItem->setData(0, SpkSidebarSelector::RoleItemIsCategory, false);
HomepageItem->setData(0, SpkSidebarSelector::RoleItemCategoryPageId, SpkStackedPages::PgHomepage);
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));
DownloadsItem = new QTreeWidgetItem(QStringList(tr("Downloads")));
DownloadsItem->setData(0, SpkSidebarSelector::RoleItemIsCategory, false);
DownloadsItem->setData(0, SpkSidebarSelector::RoleItemCategoryPageId, SpkStackedPages::PgDownloads);
#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 ============
CategoryWidget->addTopLevelItem(HomepageItem);
SidebarMgr->AddUnusableItem(CategoryParentItem);
CategoryWidget->addTopLevelItem(AppDetailsItem);
CategoryWidget->addTopLevelItem(CategoryParentItem);
CategoryWidget->addTopLevelItem(DownloadsItem);
CategoryWidget->addTopLevelItem(UiTestItem);
CategoryWidget->setFocusPolicy(Qt::NoFocus);
// 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);
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(CategoryWidget);
HorizontalDivide->addWidget(Pager);
//============ 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<int>(250 * v) + 30);
});
connect(ActClearSearchBar, &QAction::triggered, [=](){ SearchEdit->clear(); });
connect(BtnBack, &QPushButton::clicked,
[=](){
SidebarMgr->GoBack();
BtnBack->setEnabled(false);
});
auto space = static_cast<SpkWindow*>(parent)->GetTitleBar()->GetUserSpace();
space->addSpacing(50);
space->addWidget(BtnDayNight);
space->addWidget(BtnSettings);
space->addWidget(BtnBack);
space->addWidget(SearchEdit);
space->addStretch();
//============ Pages =============
// Red-Black tree based map will be able to sort things. Just for convenience of ordering pages.
QMap<SpkStackedPages, QWidget*> 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;
PageDownloads = new SpkUi::SpkPageDownloads(this);
PageDownloads->setProperty("spk_pageid", SpkStackedPages::PgDownloads);
sorter[PgDownloads] = PageDownloads;
PageSettings = new SpkUi::SpkPageSettings(this);
PageSettings->setProperty("spk_pageid", SpkStackedPages::PgSettings);
sorter[PgSettings] = PageSettings;
PageHome = new SpkUi::SpkPageHome(this);
PageSettings->setProperty("spk_pageid", SpkStackedPages::PgHomepage);
sorter[PgHomepage] = PageHome;
#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);
// Default page selection : homepage
HomepageItem->setSelected(true);
// Manually "activate" the default page item to make the sidebar tree know about default item
emit CategoryWidget->itemPressed(HomepageItem, 0);
setLayout(HorizontalDivide);
}

139
gui/spkmsgbox.cpp Normal file
View File

@@ -0,0 +1,139 @@
#include <QScrollArea>
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include "spkui_general.h"
#include "spkmainwindow.h"
#include "spkmsgbox.h"
#include "spkstore.h"
// Suppress unwanted Clazy check warnings
// clazy:excludeall=connect-3arg-lambda,lambda-in-connect
const QSize SpkMsgBox::IconSize; // I don't know why I need it, compiler wants that
SpkMsgBox::SpkMsgBox(QWidget *parent)
{
Q_UNUSED(parent);
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
}
int SpkMsgBox::StaticExec(QString msg, QString title, QMessageBox::Icon icon,
QMessageBox::StandardButtons buttons, QString extra, bool expanded)
{
SpkMsgBox *b = new SpkMsgBox(SpkStore::Instance->GetRootWindow());
QWidget *wMsgWidget = new QWidget;
QHBoxLayout *wMsg = new QHBoxLayout(wMsgWidget);
QPushButton *wExpandBtn;
QScrollArea *wExtraArea;
QLabel *wMsgText = new QLabel,
*wExtraInfo,
*wIcon;
int InitialHeight;
bool hasextra = extra.length() != 0;
if(icon)
{
wIcon = new QLabel;
QIcon icon_;
switch(icon)
{
case QMessageBox::Critical:
icon_ = QIcon::fromTheme("dialog-error");
break;
case QMessageBox::Warning:
icon_ = QIcon::fromTheme("dialog-warning");
break;
case QMessageBox::Information:
icon_ = QIcon::fromTheme("dialog-information");
break;
case QMessageBox::Question:
icon_ = QIcon::fromTheme("dialog-question");
break;
case QMessageBox::NoIcon:
break;
}
if(icon)
wIcon->setPixmap(icon_.pixmap(IconSize));
wMsg->addWidget(wIcon);
}
wMsgText->setText(msg);
wMsgText->setAlignment(Qt::AlignLeft);
wMsg->addWidget(wMsgText);
wMsg->setSpacing(10);
wMsgWidget->setLayout(wMsg);
b->AddWidget(wMsgWidget);
b->GetTitleBar()->SetTitle(title);
b->GetTitleBar()->SetOperationButton(SpkTitleBar::OperationButton::Close);
b->SetResizable(false);
b->SetCentralMargin(Margin, Margin, Margin, Margin);
b->setMaximumSize(SpkUi::PrimaryScreenSize * 0.6);
if(hasextra)
{
wExpandBtn = new QPushButton;
wExtraInfo = new QLabel;
wExtraArea = new QScrollArea;
wExtraInfo->setText(extra);
wExtraArea->setWidget(wExtraInfo);
wExtraArea->setVisible(false);
wExtraArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
wExpandBtn->setText(tr("Details"));
wExpandBtn->setMaximumWidth(100);
wExpandBtn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
wExpandBtn->setCheckable(true);
wExpandBtn->setObjectName("styChkBtn");
connect(wExpandBtn, &QPushButton::clicked,
[&](){ // FIXME: hint doesn't change when visibility changes, this is a quirky hack
wExtraArea->setVisible(wExpandBtn->isChecked());
if(wExpandBtn->isChecked())
b->setFixedHeight(b->sizeHint().height());
else
b->setFixedHeight(InitialHeight);
});
b->mBtnLay->addWidget(wExpandBtn);
b->mBtnLay->addStretch();
b->AddWidget(wExtraArea);
}
b->AddSpacing(3);
AddButtons(b, buttons);
if(hasextra)
{
b->mBtnLay->addStretch(); // Keep conventional buttons centered
if(expanded)
emit wExpandBtn->clicked();
}
InitialHeight = b->minimumSizeHint().height();
auto pos = (SpkUi::PrimaryScreenSize - b->sizeHint()) / 2;
b->move(pos.width(), pos.height());
b->setWindowModality(Qt::ApplicationModal);
b->setFixedSize(b->sizeHint());
auto r = b->Exec();
if(r != -1)
r = b->mButtonList[r]; // Retrieve the correct button
delete b;
return r;
}
void SpkMsgBox::AddButtons(SpkMsgBox *me, QMessageBox::StandardButtons b)
{
// If anyone can do it better, please let me know, I wrote this on the airplane
using btn = QMessageBox::StandardButton;
if(!b) return;
if(b.testFlag(btn::Ok)) { me->AddButton(tr("OK")); me->mButtonList << btn::Ok; };
if(b.testFlag(btn::Cancel)) { me->AddButton(tr("Cancel")); me->mButtonList << btn::Cancel; };
if(b.testFlag(btn::Yes)) { me->AddButton(tr("Yes")); me->mButtonList << btn::Yes; };
if(b.testFlag(btn::No)) { me->AddButton(tr("No")); me->mButtonList << btn::No; };
if(b.testFlag(btn::Apply)) { me->AddButton(tr("Apply")); me->mButtonList << btn::Apply; };
if(b.testFlag(btn::Reset)) { me->AddButton(tr("Reset")); me->mButtonList << btn::Reset; };
if(b.testFlag(btn::Abort)) { me->AddButton(tr("Abort")); me->mButtonList << btn::Abort; };
if(b.testFlag(btn::Retry)) { me->AddButton(tr("Retry")); me->mButtonList << btn::Retry; };
if(b.testFlag(btn::Ignore)) { me->AddButton(tr("Ignore")); me->mButtonList << btn::Ignore; };
if(b.testFlag(btn::Reset)) { me->AddButton(tr("Reset")); me->mButtonList << btn::Reset; };
if(b.testFlag(btn::Close)) { me->AddButton(tr("Close")); me->mButtonList << btn::Close; };
if(b.testFlag(btn::Open)) { me->AddButton(tr("Open")); me->mButtonList << btn::Open; };
}

35
gui/spknotifydot.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include <QPaintEvent>
#include <QPainter>
#include <QBrush>
#include "spknotifydot.h"
#include "spkstore.h"
/*
* The font size and the actual geometry of this widget is hard coded
* If you want to use this widget code please consider improving it
*/
SpkNotifyDot::SpkNotifyDot(QWidget *parent) :
QLabel(parent)
{
}
void SpkNotifyDot::paintEvent(QPaintEvent *e)
{
QPainter p(this);
int h = height();
p.setRenderHint(QPainter::Antialiasing);
p.setBrush(QBrush(QColor(255, 70, 50)));
p.setPen(Qt::transparent);
p.drawEllipse(0, 0, h, h);
p.setBrush(QBrush(QColor(Qt::white)));
p.setPen(Qt::white);
p.setFont(QFont("sansserif", 10, 1000));
p.drawText(QRect(0, 0, h, h), Qt::AlignHCenter | Qt::AlignVCenter, text());
p.end();
}

82
gui/spkpopup.cpp Normal file
View File

@@ -0,0 +1,82 @@
#include "spkpopup.h"
#include <QDebug>
namespace SpkUi
{
SpkPopup::SpkPopup(QWidget *parent, int aMillis) : QWidget(parent)
{
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint);
mText = new QLabel();
mText->setStyleSheet("border-radius: 11px;"
"background-color: rgba(0,0,0,150);"
"color: white;"
"padding: 5px;");
mText->setText(tr("(No Text)"));
mBox = new QHBoxLayout(this);
mBox->addWidget(mText);
mBox->setContentsMargins(0, 0, 0, 0);
// The reason why we contain it in a widget is that, if we want a QLabel have rounded corners,
// then it must be able to be displayed with a translucent background. However, setting
// Qt::WA_TranslucentBackground will cause the entire background of QLabel transparent.
// Therefore we need a container (SpkPopup) with a transparent background as the canvas layer
// of the actual displayed text.
mAnim = new QSequentialAnimationGroup(this);
// Disabled as translucency doesn't work well on every platform :(
// mAnimFadeIn = new QPropertyAnimation(this, "windowOpacity");
// mAnimFadeOut = new QPropertyAnimation(this, "windowOpacity");
// mAnimFadeIn->setStartValue(0.0);
// mAnimFadeIn->setEndValue(1.0);
// mAnimFadeOut->setStartValue(1.0);
// mAnimFadeOut->setEndValue(0.0);
// Using moving animation instead
mAnimFadeIn = new QPropertyAnimation(this, "pos");
mAnimFadeOut = new QPropertyAnimation(this, "pos");
mAnimFadeIn->setDuration(250);
mAnimFadeOut->setDuration(250);
mAnimFadeIn->setEasingCurve(QEasingCurve::InQuad);
mAnimFadeOut->setEasingCurve(QEasingCurve::InQuad);
mAnim->addAnimation(mAnimFadeIn);
mAnim->addPause(aMillis);
mAnim->addAnimation(mAnimFadeOut);
setVisible(false);
connect(mAnim, &QAnimationGroup::stateChanged,
[=](QAbstractAnimation::State newState, QAbstractAnimation::State oldState)
{
// qDebug() << "OldState" << oldState << "NewState" << newState;
if(newState == QAbstractAnimation::Stopped)
setVisible(false);
});
}
void SpkPopup::Show(QString aText)
{
if(mAnim->state() == QSequentialAnimationGroup::Running)
mAnim->stop();
QSize parentSize = parentWidget()->size();
mText->setText(aText);
adjustSize();
move(QPoint((parentSize.width() - width()) / 2, parentSize.height() - height() - 30)/* +
parentWidget()->pos()*/);
setMaximumWidth(parentSize.width() - 200);
setWindowOpacity(1);
show();
mAnimFadeIn->setStartValue(QPoint((parentSize.width() - width()) / 2,
parentSize.height() + height()));
mAnimFadeIn->setEndValue(QPoint((parentSize.width() - width()) / 2,
parentSize.height() - height() - 80));
mAnimFadeOut->setStartValue(QPoint((parentSize.width() - width()) / 2,
parentSize.height() - height() - 80));
mAnimFadeOut->setEndValue(QPoint((parentSize.width() - width()) / 2,
parentSize.height() + height()));
qDebug() << "Popup size " << size() << "position" << pos() << "parent size" << parentWidget()->size();
mAnim->start();
}
}

93
gui/spkqsshelper.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "spkqsshelper.h"
const std::list<SpkUi::Qss::ColorSetIndex> SpkUi::Qss::AccentColorExceptions
{
AccentColor,
AccentColorHighlighted,
TextOnAccentColor,
};
const std::map<SpkUi::Qss::ColorSetIndex, const char *> SpkUi::Qss::ColorSet2Token
{
{ GlobalBgnd, "GBG_" },
{ ControlsBgnd, "CBG_" },
{ ControlsBgndHighlighted, "CBGH" },
{ SelectionBgnd, "ACC_" },
{ SelectionBgndHighlighted, "ACCH" },
{ LightCtrlsGradLight, "LCTL1" },
{ LightCtrlsGradDark, "LCTL2" },
{ LightCtrlsGradDarker, "LCTL3" },
{ LightCtrlsDisabledBackground, "LCTLD" },
{ DarkCtrlsGradLight, "DCTL1" },
{ DarkCtrlsGradDark, "DCTL2" },
{ DarkCtrlsGradDarker, "DCTL3" },
{ DarkCtrlsDisabledBackground, "DCTLD" },
{ TextOnSelection, "TXACC" },
{ TextOnGlobalBgnd, "TXGBG" },
{ TextOnControlsBgnd, "TXCBG" },
{ TextLighter, "TXL1" },
{ TextEvenLighter, "TXL2" },
{ TextDisabled, "TXD" },
{ GlossyEdge, "GLS" },
{ ShadesEdge, "SHD" },
{ ScrollBarNorm, "SCBN" },
{ ScrollBarHover, "SCBH" },
{ DivideLine, "DVL" },
};
const std::map<SpkUi::Qss::ColorSetIndex, QColor> SpkUi::Qss::DarkColorSet
{
{ GlobalBgnd, 0x282828 },
{ ControlsBgnd, 0x323232 },
{ ControlsBgndHighlighted, 0xff0000 },
{ SelectionBgnd, 0x0070ff },
{ SelectionBgndHighlighted, QColor(0x0070ff).lighter(120) },
{ LightCtrlsGradLight, 0x6b6b6b },
{ LightCtrlsGradDark, 0x656565 },
{ LightCtrlsGradDarker, 0x606060 },
{ LightCtrlsDisabledBackground, 0x808080 },
{ DarkCtrlsGradLight, 0x404040 },
{ DarkCtrlsGradDark, 0x383838 },
{ DarkCtrlsGradDarker, 0x323232 },
{ DarkCtrlsDisabledBackground, 0x525252 },
{ TextOnSelection, ColorTextOnBackground(0x0070ff) },
{ TextOnGlobalBgnd, ColorTextOnBackground(0x282828) },
{ TextOnControlsBgnd, ColorTextOnBackground(0x282828) },
{ TextLighter, 0xd5d5d5 },
{ TextEvenLighter, 0x505050 },
{ TextDisabled, 0xbebebe },
{ GlossyEdge, 0x656565 },
{ ShadesEdge, 0x7b7b7b },
{ ScrollBarNorm, 0x404040 },
{ ScrollBarHover, 0x656565 },
{ DivideLine, 0x424242 },
};
const std::map<SpkUi::Qss::ColorSetIndex, QColor> SpkUi::Qss::LightColorSet
{
{ GlobalBgnd, 0xf8f8f8 },
{ ControlsBgnd, 0xf8f8f8 },
{ ControlsBgndHighlighted, 0xff0000 },
{ SelectionBgnd, 0x0070ff },
{ SelectionBgndHighlighted, QColor(0x0070ff).lighter(120) },
{ LightCtrlsGradLight, 0xfbfbfb },
{ LightCtrlsGradDark, 0xf2f2f2 },
{ LightCtrlsGradDarker, 0xebebeb },
{ LightCtrlsDisabledBackground, 0xe0e0e0 },
{ DarkCtrlsGradLight, 0xe4e4e4 },
{ DarkCtrlsGradDark, 0xcecece },
{ DarkCtrlsGradDarker, 0xb8b8b8 },
{ DarkCtrlsDisabledBackground, 0xababab },
{ TextOnSelection, ColorTextOnBackground(0x0070ff) },
{ TextOnGlobalBgnd, ColorTextOnBackground(0xf8f8f8) },
{ TextOnControlsBgnd, ColorTextOnBackground(0xf8f8f8) },
{ TextLighter, 0x2a2a2a },
{ TextEvenLighter, 0xa0a0a0 },
{ TextDisabled, 0x8a8a8a },
{ GlossyEdge, 0x9d9d9d },
{ ShadesEdge, 0xc5c5c5 },
{ ScrollBarNorm, 0xa0a0a0 },
{ ScrollBarHover, 0x858585 },
{ DivideLine, 0xd5d5d5 },
};

31
gui/spksidebartree.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include <QMouseEvent>
#include "spksidebartree.h"
SpkUi::SpkSidebarTree::SpkSidebarTree(QWidget *parent) :
QTreeWidget(parent)
{
}
void SpkUi::SpkSidebarTree::mouseMoveEvent(QMouseEvent *e)
{
// This is solely for forcibly disabling the view to change selection when dragging on the view
// and probably the only reason why this class began its existence
if((e->buttons() & Qt::LeftButton))
setState(NoState);
else
QTreeWidget::mouseMoveEvent(e);
}
void SpkUi::SpkSidebarTree::mousePressEvent(QMouseEvent *e)
{
// Prevent anything being deselected
if(e->modifiers().testFlag(Qt::ControlModifier) && e->buttons().testFlag(Qt::LeftButton))
{
auto i = itemAt(e->pos());
if(i && i->isSelected())
return;
}
QTreeWidget::mousePressEvent(e);
}

92
gui/spkstretchlayout.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "spkstretchlayout.h"
SpkStretchLayout::SpkStretchLayout(QWidget *parent) : QLayout(parent)
{
}
SpkStretchLayout::~SpkStretchLayout()
{
QLayoutItem *item;
while((item = takeAt(0)))
delete item;
}
void SpkStretchLayout::addItem(QLayoutItem *item)
{
mItems.append(item);
}
QSize SpkStretchLayout::sizeHint() const
{
if(mItems.isEmpty())
return { 300, 300 };
auto w = geometry().width() - spacing();
auto it = mItems.first();
int countPerLine = w / (it->minimumSize().width() + spacing());
int lines = ceil((double)mItems.size() / countPerLine);
auto h = static_cast<int>(it->minimumSize().height() * lines + spacing() * lines);
return { w, h };
}
QSize SpkStretchLayout::minimumSize() const
{
// It works this way, but I honestly have no idea WHY IT WORKS
auto r = sizeHint();
r.setWidth(300);
return r;
}
int SpkStretchLayout::count() const
{
return mItems.size();
}
QLayoutItem *SpkStretchLayout::itemAt(int i) const
{
return mItems.value(i);
}
QLayoutItem *SpkStretchLayout::takeAt(int i)
{
return i >= 0 && i < mItems.size() ? mItems.takeAt(i) : nullptr;
}
void SpkStretchLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
if(mItems.isEmpty())
return;
int spc = spacing(), w = rect.width() - spc;
QSize size;
auto itm = mItems.first();
// All items are considered the same, so we only calculate with the first item.
// Figure out how many at most can we squeeze into one line
int countPerLine = w / (itm->minimumSize().width() + spacing());
if(countPerLine < 1)
countPerLine = 1;
if(countPerLine >= mItems.size()) // All items fit in one line
size = itm->minimumSize();
// Won't fit in one line.
else // Stretch items.
size = QSize {(w / countPerLine - spc), itm->maximumSize().height() };
auto origin = geometry().topLeft();
QLayoutItem *o;
for(int i = 0; i < mItems.size(); i++)
{
o = mItems.at(i);
QRect geo;
geo.setSize(size);
geo.moveTo((i % countPerLine) * (size.width() + spc) + spc + origin.x(),
(i / countPerLine) * (size.height() + spacing()) + spc + origin.y());
o->setGeometry(geo);
}
// qDebug() << rect;
}

149
gui/spktitlebar.cpp Normal file
View File

@@ -0,0 +1,149 @@
#include <QEvent>
#include <QMouseEvent>
#include "spkwindow.h"
#include "spkui_general.h"
#include "spktitlebar.h"
SpkTitleBar::SpkTitleBar(QWidget *parent) : QFrame(parent)
{
mLinkedWindow = nullptr;
setFixedHeight(Height);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
mIcon = new QLabel(this);
mTitle = new QLabel(this);
mIcon->setFixedSize(40, 40);
mMainLayout = new QHBoxLayout(this);
mUserSpace = new QHBoxLayout();
mBtnGroup = new QHBoxLayout();
mBtnMin = new SpkTitleBarDefaultButton(this);
mBtnMaxRestore = new SpkTitleBarDefaultButton(this);
mBtnClose= new SpkTitleBarDefaultButton(this);
mMainLayout->setSpacing(8);
mBtnGroup->setSpacing(0);
mMainLayout->addSpacing(12);
mMainLayout->addWidget(mIcon);
mMainLayout->addWidget(mTitle);
mMainLayout->addLayout(mUserSpace);
mMainLayout->addLayout(mBtnGroup);
mBtnGroup->addWidget(mBtnMin);
mBtnGroup->addWidget(mBtnMaxRestore);
mBtnGroup->addWidget(mBtnClose);
mBtnMin->SetRole(OperationButton::Minimize);
mBtnMaxRestore->SetRole(OperationButton::MaximizeRestore);
mBtnClose->SetRole(OperationButton::Close);
mMainLayout->setContentsMargins(0, 0, 0, 1);
setLayout(mMainLayout);
connect(mBtnClose, &QPushButton::clicked, this, &SpkTitleBar::CloseWindow);
connect(mBtnMin, &QPushButton::clicked, this, &SpkTitleBar::MinimizeWindow);
connect(mBtnMaxRestore, &QPushButton::clicked, this, &SpkTitleBar::MaximizeRestoreWindow);
}
SpkTitleBar::~SpkTitleBar()
{
//qDebug() << "Freed title bar!";
}
void SpkTitleBar::SetOperationButton(OperationButton type)
{
mBtnClose->setVisible(type & OperationButton::Close);
mBtnMaxRestore->setVisible(type & OperationButton::MaximizeRestore);
mBtnMin->setVisible(type & OperationButton::Minimize);
}
bool SpkTitleBar::event(QEvent *evt)
{
switch(evt->type())
{
case QEvent::MouseButtonDblClick:
{
if(static_cast<QMouseEvent*>(evt)->button())
emit mBtnMaxRestore->clicked();
break;
}
default:;
}
return QWidget::event(evt);
}
void SpkTitleBar::CloseWindow()
{
if(mLinkedWindow)
mLinkedWindow->close();
}
void SpkTitleBar::MinimizeWindow()
{
if(mLinkedWindow)
mLinkedWindow->setWindowState(Qt::WindowMinimized);
}
void SpkTitleBar::MaximizeRestoreWindow()
{
if(mLinkedWindow)
{
if(mLinkedWindow->windowState().testFlag(Qt::WindowMaximized))
mLinkedWindow->setWindowState(mLinkedWindow->windowState() & ~Qt::WindowMaximized);
else
mLinkedWindow->setWindowState(Qt::WindowMaximized);
}
}
SpkTitleBarDefaultButton::SpkTitleBarDefaultButton(QWidget* parent) : QPushButton(parent)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
setMaximumWidth(ButtonWidth);
setMinimumWidth(ButtonWidth);
setFocusPolicy(Qt::NoFocus);
}
void SpkTitleBarDefaultButton::paintEvent(QPaintEvent *e)
{
QPushButton::paintEvent(e);
QPainter painter;
painter.begin(this);
PaintSymbol(painter);
painter.end();
}
void SpkTitleBarDefaultButton::PaintSymbol(QPainter &p)
{
QPen pen(SpkUi::ColorLine);
p.setPen(pen);
auto mh = height() / 2, mw = width() / 2, h = height(), w = width();
constexpr int fr = 10;
switch(Role)
{
case Minimize:
p.drawLine(mw - w / fr, mh, mw + w / fr, mh);
break;
case MaximizeRestore:
p.drawRect(mw - w / fr, mh - h / fr,
w / fr * 2, h / fr * 2);
break;
case Restore:
p.drawRect(mw - w / fr - 2, mh - w / fr + 2, w / fr * 2, h / fr * 2);
p.drawLine(mw - w / fr, mh - h / fr - 2, mw + w / fr, mh - h / fr - 2);
p.drawLine(mw + w / fr, mh - h / fr - 2, mw + w / fr, mh + h / fr - 2);
break;
case Close:
p.drawLine(mw - h / fr * 1.3, mh - h / fr * 1.3,
mw + h / fr * 1.3, mh + h / fr * 1.3);
p.drawLine(mw - h / fr * 1.3, mh + h / fr * 1.3,
mw + h / fr * 1.3, mh - h / fr * 1.3);
break;
}
}
void SpkTitleBarDefaultButton::SetRole(OperationButton role)
{
Role = role;
}

332
gui/spkui_general.cpp Normal file
View File

@@ -0,0 +1,332 @@
//
// Created by rigoligo on 2021/5/8.
//
#include <QApplication>
#include <QFile>
#include <QTextStream>
#include <QLibrary>
#include <QDir>
#include <QMessageBox>
#include <QDateTime>
#include <QDebug>
#include <QScreen>
#include <QPluginLoader>
#include <QStyleFactory>
#include <csignal>
#include <execinfo.h>
#include "spkui_general.h"
#include "spkmsgbox.h"
#include "spkpopup.h"
#include "spklogging.h"
#include "spkstore.h"
#include "spkutils.h"
namespace SpkUi
{
UiMetaObject SpkUiMetaObject;
SpkUiStyle CurrentStyle;
QString StylesheetBase, CurrentStylesheet;
QColor ColorLine, ColorBack, ColorBtnMaskSelected, ColorBtnMaskUnselected;
QSize PrimaryScreenSize;
SpkDtkPlugin *DtkPlugin = nullptr;
QStyle *OldSystemStyle = nullptr;
std::map<Qss::ColorSetIndex, QColor> CurrentColorSet;
SpkPopup *Popup;
namespace States
{
bool IsDDE = false, IsUsingDtkPlugin = false;
bool DoRespondAutoTheme = false;
int LightDarkMode = 3; // Default to Manual
bool ThemeConfigCallback()
{
switch(LightDarkMode)
{
case 0:
if(!DtkPlugin) return false;
SpkUiMetaObject.SetDarkLightTheme(DtkPlugin->GetIsDarkTheme());
break;
case 1:
SetGlobalStyle(Light, true);
SpkUiMetaObject.SetThemeButtonVisible(false);
break;
case 2:
SetGlobalStyle(Dark, true);
SpkUiMetaObject.SetThemeButtonVisible(false);
break;
case 3:
SpkUiMetaObject.SetThemeButtonVisible(true);
break;
}
return true;
}
}
namespace Priv
{
bool CrashHandlerActivated;
}
// ======================= Static Linkage Private Functions ========================
static void SetBtnMaskColor()
{
ColorBtnMaskUnselected = ColorTextOnBackground(CurrentColorSet[Qss::ControlsBgnd]);
ColorBtnMaskSelected = ColorTextOnBackground(CurrentColorSet[Qss::AccentColor]);
}
// ======================== Public Functions =========================
void Initialize()
{
// Obtain global stylesheets
QFile ObtainStylesheet;
ObtainStylesheet.setFileName(CFG->ReadField("internal/qss_path",
":/stylesheet/stylesheet/default.css")
.toString());
ObtainStylesheet.open(QIODevice::ReadOnly);
StylesheetBase = ObtainStylesheet.readAll();
ObtainStylesheet.close();
CurrentStyle = SpkUiStyle::Invalid;
#ifdef NDEBUG
SetGlobalStyle(Light, false);
#else
SetGlobalStyle(qEnvironmentVariableIntValue("SPK_FORCE_DARK") ? Dark : Light, false);
#endif
// Initalize crash handler
signal(SIGSEGV, SpkUi::CrashSignalHandler);
signal(SIGABRT, SpkUi::CrashSignalHandler);
signal(SIGFPE, SpkUi::CrashSignalHandler);
// Prepare theme following for DDE
if((States::IsDDE = CheckIsDeepinDesktop()))
PrepareForDeepinDesktop();
// Misc data initialization
PrimaryScreenSize = QGuiApplication::primaryScreen()->size();
CFG->BindField("ui/theme", &States::LightDarkMode, 3, States::ThemeConfigCallback);
}
void GuessAppropriateTheme()
{
// FIXME: Too difficult, not implementing it
}
bool CheckIsDeepinDesktop()
{
QString Desktop(getenv("XDG_CURRENT_DESKTOP"));
// This method of checking is from DTK source code.
if(Desktop.contains("deepin", Qt::CaseInsensitive) ||
Desktop.contains("tablet", Qt::CaseInsensitive))
return true;
else
return false;
}
void PrepareForDeepinDesktop()
{
#ifndef NDEBUG
qApp->addLibraryPath(qApp->applicationDirPath() + "/plugin/dtkplugin");
#else
// You must `make install' before these work in release mode
qApp->addLibraryPath("/usr/local/lib");
qApp->addLibraryPath("/usr/lib");
#endif
if(!qEnvironmentVariableIntValue("SPARK_NO_DTK_PLUGIN"))
{
QPluginLoader p("libspkdtkplugin");
if(p.load())
{
auto i = qobject_cast<SpkDtkPlugin*>(p.instance());
if(i)
{
DtkPlugin = i;
States::IsUsingDtkPlugin = true;
i->Initialize();
SpkUiMetaObject.SetAccentColor(i->GetAccentColor()); // Match OS accent color
SpkUiMetaObject.SetDarkLightTheme(i->GetIsDarkTheme()); // Match OS dark theme type
QObject::connect(i, &SpkDtkPlugin::AccentColorChanged,
&SpkUiMetaObject, &UiMetaObject::SetAccentColor);
QObject::connect(i, &SpkDtkPlugin::DarkLightThemeChanged,
&SpkUiMetaObject, &UiMetaObject::SetDarkLightTheme);
}
}
}
// NOTE: Chameleon style kept adding unwanted blue focus indication border
// to widgets that shouldn't have borders.
// We need to eliminate this irritating problem.
if(qEnvironmentVariableIntValue("SPARK_NO_QSTYLE_CHANGE"))
return;
OldSystemStyle = QStyleFactory::create("chameleon"); // TreeWidget doesn't work well with Fusion
auto styles = QStyleFactory::keys();
styles.removeAll("chameleon");
if(styles.contains("Fusion"))
{
auto style = QStyleFactory::create("Fusion");
qApp->setStyle(style);
}
else if(styles.size()) // What? This shouldn't happen.
qApp->setStyle(QStyleFactory::create(styles[0]));
else // Duh...
sWarn(QObject::tr("Cannot find styles other than 'chameleon'! You may see widgets "
"with unwanted blue borders."));
}
void SetGlobalStyle(const SpkUiStyle aStyle, const bool aPreserveAccentColor)
{
if(aStyle == CurrentStyle) // Don't waste precious CPU time parsing new style sheet!
return;
CurrentStyle = aStyle;
Qss::ColorSet tempset;
switch(aStyle)
{
case Invalid:
case Light:
tempset = Qss::LightColorSet;
ColorLine = Qt::black;
break;
case Dark:
tempset = Qss::DarkColorSet;
ColorLine = Qt::white;
break;
}
if(aPreserveAccentColor)
{
for(auto i : Qss::AccentColorExceptions)
tempset[i] = CurrentColorSet[i];
}
CurrentColorSet = tempset;
CurrentStylesheet = StylesheetFromColors(CurrentColorSet);
SetBtnMaskColor();
qApp->setStyleSheet(CurrentStylesheet);
}
QString WriteStackTrace(const QString &aStackTrace)
{
QString path = QDir::homePath() + "/.local/share/spark-store/crash/";
QFile StackTraceFile;
if(!QDir().exists(path))
if(!QDir().mkpath(path))
return QObject::tr("Stack trace directory %1 cannot be created. "
"Stack trace wasn't saved.").arg(path);
path += QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
while(QFile::exists(path))
path += "_";
StackTraceFile.setFileName(path);
StackTraceFile.open(QIODevice::WriteOnly);
if(StackTraceFile.isOpen() && StackTraceFile.isWritable())
{
QTextStream StackTraceWriter;
StackTraceWriter.setDevice(&StackTraceFile);
StackTraceWriter << QDateTime::currentDateTime().toLocalTime().toString() << "\n\n";
StackTraceWriter << aStackTrace;
StackTraceFile.close();
return QObject::tr("Stack trace written to \"%1\".").arg(path);
}
return QObject::tr("Stack trace file %1 cannot be opened. "
"Stack trace wasn't saved.").arg(path);
}
void CrashSignalHandler(int sig)
{
QString msg(QObject::tr("Program has received signal %1 during normal execution.\n\n")),
trace;
switch(sig)
{
case SIGSEGV:
msg = msg.arg(QObject::tr("\"SIGSEGV\" (Segmentation fault)"));
goto CRASH;
case SIGFPE:
msg = msg.arg(QObject::tr("\"SIGFPE\" (Arithmetic exception)"));
goto CRASH;
case SIGABRT:
msg = msg.arg(QObject::tr("\"SIGABRT\" (Abort)"));
CRASH:
{
if(Priv::CrashHandlerActivated) // If error occured in the handler...
{
signal(SIGABRT, SIG_DFL); // Return control flow to OS and give up
raise(SIGABRT);
exit(2);
}
Priv::CrashHandlerActivated = true;
void* TraceStack[StackTraceArraySize];
int StackTraceSize = backtrace(TraceStack, StackTraceArraySize);
auto TraceTextArray = backtrace_symbols(TraceStack, StackTraceArraySize);
trace = QString(QObject::tr("Stack trace:\n"));
for(int i = 0; i < StackTraceSize; i++)
trace += QString::number(i) + "> " + QString(TraceTextArray[i]) + '\n';
msg += QObject::tr("Spark Store cannot continue.\n\n");
msg += WriteStackTrace(trace);
SpkMsgBox::StaticExec(msg, QObject::tr("Spark Store Crashed"), QMessageBox::Critical,
QMessageBox::Ok, trace);
exit(2);
}
default:
sErrPop(QObject::tr("Unknown signal %1 received in crash handler. "
"Program internals may be corrupted. Please decide if you want "
"to continue execution.").arg(sig));
}
}
QIcon GetThemedIcon(QString name)
{
if(CurrentStyle == SpkUiStyle::Dark)
name += "-dark";
return QIcon(":/icons/" + name + ".svg");
}
QString StylesheetFromColors(Qss::ColorSet aColors)
{
QString ret = StylesheetBase;
foreach(auto &i, aColors)
ret = ret.replace(Qss::ColorSet2Token.at(i.first), i.second.name());
return ret;
}
QColor ColorTextOnBackground(QColor c)
{
// From https://github.com/feiyangqingyun/qtkaifajingyan
double gray = (0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()) / 255;
return gray > 0.5 ? Qt::black : Qt::white;
}
// =================== UiMetaObject =======================
// UiMetaObject is the signal-slot receiver for DDE plugin, receiving the DDE system level
// notifications of UI theme changes
// Communications with UI widgets are also done here
void UiMetaObject::SetAccentColor(QColor aColor)
{
if(!SpkUi::States::DoRespondAutoTheme)
return;
CurrentColorSet[Qss::AccentColor] = aColor.lighter(90);
CurrentColorSet[Qss::AccentColorHighlighted] = aColor.lighter(105);
CurrentColorSet[Qss::TextOnAccentColor] = ColorTextOnBackground(aColor);
SetBtnMaskColor();
qApp->setStyleSheet(StylesheetFromColors(CurrentColorSet));
}
void UiMetaObject::SetDarkLightTheme(bool isDark)
{
if(!SpkUi::States::DoRespondAutoTheme)
return;
if(isDark)
SetGlobalStyle(Dark, true);
else
SetGlobalStyle(Light, true);
}
}

326
gui/spkwindow.cpp Normal file
View File

@@ -0,0 +1,326 @@
#include <QVBoxLayout>
#include <QApplication>
#include <QStyleHints>
#include <QPainterPath>
#include <QFile>
#include <QPushButton>
#include <QMouseEvent>
#include "spklogging.h"
#include "spkwindow.h"
#include "spkui_general.h"
#include "spktitlebar.h"
#include <QDebug>
SpkWindow::SpkWindow(QWidget *parent) : QWidget(parent)
{
mUseCustomEvents = SpkUi::DtkPlugin == nullptr; // Put to the front, to prevent warnings
if(SpkUi::DtkPlugin && !qEnvironmentVariableIntValue("SPARK_NO_DXCB"))
SpkUi::DtkPlugin->addWindow(this, parent); // Register window to DXcb so we got Deepin
else
setWindowFlags(Qt::FramelessWindowHint); // Remove default title bar
setAttribute(Qt::WA_Hover);
mCornerRadius = 5;
mUserCentralWidget = nullptr;
mResizable = true;
mMoving = mResizing = false;
mCloseHook = nullptr;
mMaximized = windowState().testFlag(Qt::WindowMaximized);
setMouseTracking(true);
PopulateUi();
}
SpkWindow::~SpkWindow()
{
}
bool SpkWindow::event(QEvent *evt)
{
// These custom events weren't useful for Deepin platform
if(!mUseCustomEvents)
return QWidget::event(evt);
switch(evt->type())
{
case QEvent::WindowStateChange:
{
mMaximized = windowState().testFlag(Qt::WindowMaximized);
if(mMaximized)
mTitleBarComponent->mBtnMaxRestore->SetRole(SpkTitleBarDefaultButton::Restore);
else
mTitleBarComponent->mBtnMaxRestore->SetRole(SpkTitleBarDefaultButton::MaximizeRestore);
break;
}
case QEvent::MouseButtonPress:
{
// if(!mResizable) break;
auto e = static_cast<QMouseEvent*>(evt);
if(e->button() != Qt::LeftButton) break;
auto edge = DetectEdgeOnThis(e->pos());
if(edge)
{
mResizing = true;
mEdgesBeingResized = edge;
return true;
}
else
{
if(!QWidget::event(evt) || 1) // Movable property is not implemented, let move anywhere
{
mMoveOffset = e->globalPos() - pos();
mMoving = true;
mResizing = false;
}
return true;
}
break;
}
case QEvent::MouseButtonRelease:
{
// if(!mResizable) break;
auto e = static_cast<QMouseEvent*>(evt);
if(e->button() != Qt::LeftButton) break;
mResizing = false;
mMoving = false;
unsetCursor();
return true;
break;
}
case QEvent::HoverMove:
{
if((mResizing || !mResizable) && !mMoving) break;
if(mMaximized)
{
unsetCursor();
break;
}
if(mResizable && !mMoving)
{
auto e = static_cast<QHoverEvent*>(evt);
auto edge = DetectEdgeOnThis(e->pos());
SetMouseCursor(edge);
}
break;
}
case QEvent::MouseMove:
{
if(mMaximized) break;
auto e = static_cast<QMouseEvent*>(evt);
if(mResizing && mResizable)
{
ResizeWindowByCursor(e->globalPos());
return true; // Intercept resize movements
}
else if(mMoving)
{
move(e->globalPos() - mMoveOffset);
}
break;
}
default:
;
}
return QWidget::event(evt);
}
Qt::Edges SpkWindow::DetectEdgeOnThis(QPoint p)
{
Qt::Edges edge;
if(p.x() < BorderWidth) edge |= Qt::LeftEdge;
if(p.x() > width() - BorderWidth) edge |= Qt::RightEdge;
if(p.y() < BorderWidth) edge |= Qt::TopEdge;
if(p.y() > height() - BorderWidth) edge |= Qt::BottomEdge;
return edge;
}
void SpkWindow::SetMouseCursor(Qt::Edges e)
{
switch(e)
{
case Qt::TopEdge:
case Qt::BottomEdge:
setCursor(Qt::SizeVerCursor);
break;
case Qt::LeftEdge:
case Qt::RightEdge:
setCursor(Qt::SizeHorCursor);
break;
case Qt::TopEdge | Qt::LeftEdge:
case Qt::BottomEdge | Qt::RightEdge:
setCursor(Qt::SizeFDiagCursor);
break;
case Qt::TopEdge | Qt::RightEdge:
case Qt::BottomEdge | Qt::LeftEdge:
setCursor(Qt::SizeBDiagCursor);
break;
default:
unsetCursor();
}
}
void SpkWindow::ResizeWindowByCursor(QPoint p)
{
auto r_ = geometry(), r = r_;
switch(mEdgesBeingResized)
{
case Qt::TopEdge | Qt::LeftEdge:
r.setLeft(p.x());
if(r.width() < minimumWidth()) // If smaller than minimum the window moves, so we stop it
r.setLeft(r_.left());
case Qt::TopEdge:
r.setTop(p.y());
if(r.height() < minimumHeight()) // Same
r.setTop(r_.top());
break;
case Qt::BottomEdge | Qt::LeftEdge:
r.setLeft(p.x());
if(r.width() < minimumWidth())
r.setLeft(r_.left());
case Qt::BottomEdge:
r.setBottom(p.y());
if(r.height() < minimumHeight())
r.setBottom(r_.bottom());
break;
case Qt::TopEdge | Qt::RightEdge:
r.setTop(p.y());
r.setRight(p.x());
if(r.height() < minimumHeight())
r.setTop(r_.top());
if(r.width() < minimumWidth())
r.setRight(r_.right());
break;
case Qt::BottomEdge | Qt::RightEdge:
r.setBottom(p.y());
if(r.height() < minimumHeight())
r.setBottom(r_.bottom());
case Qt::RightEdge:
r.setRight(p.x());
if(r.width() < minimumWidth())
r.setRight(r_.right());
break;
case Qt::LeftEdge:
r.setLeft(p.x());
if(r.width() < minimumWidth())
r.setLeft(r_.left());
break;
default:
return;
}
setGeometry(r);
}
void SpkWindow::closeEvent(QCloseEvent *e)
{
if(mCloseHook)
{
if(mCloseHook())
e->accept();
else
e->ignore();
emit Closed();
return;
}
e->accept();
emit Closed();
}
void SpkWindow::paintEvent(QPaintEvent *e)
{
QWidget::paintEvent(e);
if(!mUseCustomEvents)
return;
QPainter painter(this);
painter.setBrush(QBrush(Qt::NoBrush));
painter.setPen(QPen(SpkUi::ColorLine));
QRect rect = this->rect();
rect.adjust(0, 0, -1, -1);
painter.drawRect(rect);
}
void SpkWindow::SetCornerRadius(int radius)
{
mCornerRadius = radius;
}
void SpkWindow::PopulateUi()
{
mMainVLayout = new QVBoxLayout;
mTitleBarComponent = new SpkTitleBar(this);
setLayout(mMainVLayout);
mMainVLayout->addWidget(mTitleBarComponent);
mMainVLayout->setAlignment(Qt::AlignTop);
if(mUseCustomEvents)
mMainVLayout->setContentsMargins(1, 1, 1, 1);
else
mMainVLayout->setContentsMargins(0, 0, 0, 0);
mMainVLayout->setSpacing(0);
mTitleBarComponent->SetTitle(qAppName());
mTitleBarComponent->SetUseIcon(false);
mTitleBarComponent->SetLinkedWindow(this);
setWindowFlag(Qt::NoDropShadowWindowHint, false);
}
void SpkWindow::SetCentralWidget(QWidget *widget)
{
if(mUserCentralWidget)
mMainVLayout->removeWidget(mUserCentralWidget);
mUserCentralWidget = widget;
mMainVLayout->addWidget(widget);
}
void SpkWindow::SetUseTitleBar(bool x)
{
mTitleBarComponent->setVisible(x);
}
void SpkWindow::SetCentralMargin(int a, int b, int c, int d)
{
if(mUserCentralWidget)
mUserCentralWidget->setContentsMargins(a, b, c, d);
}
void SpkWindow::SetCloseHook(bool (*f)())
{
mCloseHook = f;
}
void SpkWindow::ClearCloseHook()
{
mCloseHook = nullptr;
}
void SpkWindow::RecalculateSize()
{
mMainVLayout->activate();
}
bool SpkWindow::GetUseTitleBar()
{
return mTitleBarComponent->isVisible();
}
SpkTitleBar *SpkWindow::GetTitleBar()
{
return mTitleBarComponent;
}
SpkTitleBar *SpkWindow::SetTitleBar(SpkTitleBar *bar, bool replace)
{
if(!bar)
return nullptr;
auto ret = mTitleBarComponent;
mMainVLayout->removeWidget(mTitleBarComponent);
if(replace)
mMainVLayout->insertWidget(0, bar);
mTitleBarComponent = bar;
bar->SetLinkedWindow(this);
return ret;
}

9
inc/deepinplatform.h Normal file
View File

@@ -0,0 +1,9 @@
//
// Created by rigoligo on 2021/5/8.
//
#pragma once
// Stub
#endif //_DEEPINPLATFORM_H_

22
inc/dtk/spkdtkplugin.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <QWidget>
class SpkDtkPlugin : public QObject
{
Q_OBJECT
public:
virtual ~SpkDtkPlugin() = default;
virtual void Initialize() = 0;
virtual void addWindow(QWidget* w, QObject* parent) = 0;
virtual QColor GetAccentColor() = 0;
virtual bool GetIsDarkTheme() = 0;
signals:
void AccentColorChanged(QColor);
void DarkLightThemeChanged(bool isDark);
};
QT_BEGIN_NAMESPACE
Q_DECLARE_INTERFACE(SpkDtkPlugin, "org.spark-store.client.dtkplugin")
QT_END_NAMESPACE

8
inc/gitver.h Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
namespace GitVer
{
const char *DescribeTags();
const char *CommitDate();
}

View File

@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QLabel>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QFormLayout>
#include "page/spkpagebase.h"
#include "spkstretchlayout.h"
#include "spkimgviewer.h"
namespace SpkUi
{
class SpkDetailEntry;
class SpkClickLabel : public QLabel
{
Q_OBJECT
protected:
virtual void mousePressEvent(QMouseEvent *e) override { emit Pressed(); }
signals:
void Pressed();
};
class SpkPageAppDetails : public SpkPageBase
{
Q_OBJECT
public:
SpkPageAppDetails(QWidget *parent = nullptr);
~SpkPageAppDetails();
void LoadAppResources(QString pkgName, QString icon, QStringList screenshots, QStringList tags);
void SetWebsiteLink(QString url);
void SetPackagePath(QString url);
private:
QString mPkgPath;
QPixmap mBrokenImg, mIconLoading;
public slots:
void ResourceAcquisitionFinished(int id, ResourceResult result);
void Activated();
public:
static constexpr QSize IconSize { 144, 144 };
// Main Area
QScrollArea *mMainArea, *mScreenshotArea;
QWidget *mDetailWidget, *mIconTitleWidget, *mWid4MainArea, *mWid4ShotArea;
QLabel *mAppTitle, *mAppIcon, *mAppDescription, *mAppShortDesc, *mPkgName, *mVersion,
*mWebsite;
SpkDetailEntry *mAuthor, *mContributor, *mSite, *mArch, *mSize;
SpkStretchLayout *mDetailLay;
QVBoxLayout *mDetailsLay, *mTitleLay, *mMainLay;
QHBoxLayout *mIconTitleLay, *mScreenshotLay;
QList<SpkClickLabel*> mScreenshotPreviews;
QMap<int, QPixmap> mAppImages;
SpkImgViewer *mImgViewer;
// Bottom bar
QWidget *mBottomBar;
QPushButton *mBtnInstall, *mBtnDownload, *mBtnUninstall, *mBtnRequestUpdate, *mBtnReport;
QHBoxLayout *mBottomBarLay;
signals:
void RequestDownload(QString name, QString pkgName, QString path);
private slots:
void ImageClicked();
};
class SpkDetailEntry : public QWidget
{
Q_OBJECT
public:
SpkDetailEntry(QWidget *parent = nullptr);
void SetTitle(const QString &s) { mTitle.setText(s); }
void SetValue(const QString &s) { mField.setText(s); }
private:
QLabel mTitle, mField;
QHBoxLayout mLay;
};
}

63
inc/page/spkpageapplist.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <QtWidgets>
#include "spkresource.h"
#include "spkappitem.h"
#include "page/spkpagebase.h"
#include "spkstretchlayout.h"
namespace SpkUi
{
class SpkPageAppList : public SpkPageBase
{
Q_OBJECT
public:
SpkPageAppList(QWidget *parent = nullptr);
void AddApplicationEntry(QString name, QString pkgName, QString description, QString iconUrl,
int appId);
void ClearAll();
void SetPageStatus(int total, int current, int itemCount, QString &keyword);
void SetCurrentCategory(int categoryId) { mCategoryId = categoryId; }
private:
void DisablePageSwitchers();
public:
private:
QVBoxLayout *mMainLay;
QHBoxLayout *mPageSwitchLay;
QPushButton *mBtnPgUp, *mBtnPgDown, *mBtnGotoPage;
QLineEdit *mPageInput;
QScrollArea *mAppsArea;
QLabel *mPageIndicator;
QWidget *mAppsWidget, *mPageSwitchWidget;
SpkStretchLayout *mItemLay;
QList<SpkAppItem *> mAppItemList;
// Cached icons
QPixmap *mLoadingIcon,
*mBrokenIcon;
QIntValidator *mPageValidator;
int mCategoryId, mCurrentPage;
QString mKeyword;
signals:
void ApplicationClicked(int appId);
void SwitchListPage(int categoryId, int page);
void SwitchSearchPage(QString keyword, int page);
public slots:
void ResourceAcquisitionFinished(int id, ResourceResult result);
void Activated();
private slots:
void PageUp();
void PageDown();
void GotoPage();
};
}

27
inc/page/spkpagebase.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QtWidgets>
#include <spkresource.h>
class SpkPageBase : public QWidget
{
Q_OBJECT
public:
SpkPageBase(QWidget *parent = nullptr);
public slots:
/**
* @brief This signal is emitted by resource manager when a resource acquisition requested
* has finished.
* @param id The request ID
* @param result The data retrieved
*/
virtual void ResourceAcquisitionFinished(int id, ResourceResult result);
/**
* @brief This is an optional signal for Resource Context objects, mainly used for notifying the
* context that it is now activated (therefore it needs to acquire the resource manager).
*/
virtual void Activated();
};

View File

@@ -0,0 +1,45 @@
#ifndef SPKPAGEDOWNLOADS_H
#define SPKPAGEDOWNLOADS_H
#include "spkdownload.h"
#include "spkdownloadentry.h"
#include "page/spkpagebase.h"
#include "pkgs/spkpkgmgrbase.h"
namespace SpkUi
{
class SpkPageDownloads : public SpkPageBase
{
Q_OBJECT
public:
SpkPageDownloads(QWidget *parent = nullptr);
~SpkPageDownloads();
public slots:
void AddDownloadTask(QString name, QString pkgName, QString path);
private:
// Logic
SpkDownloadMgr *mDownloadMgr;
QMap<uint, SpkDownloadEntry*> mEntries;
uint mNextDownloadId;
QQueue<QPair<int, QString>> mWaitingDownloads;
enum { Idle, Waiting, Downloading } mCurrentStatus;
// UI
QVBoxLayout *mLayEntries, *mMainLay;
QWidget *mScrollWidget;
QScrollArea *mScrollArea;
private slots:
void DownloadProgress(qint64 downloadedBytes, qint64 totalBytes, int id);
void DownloadStopped(SpkDownloadMgr::TaskResult status, int id);
void EntryAction(SpkDownloadEntry::EntryAction);
void InstallationEnded(int id, SpkPkgMgrBase::PkgInstallResult, int exitCode);
private:
void NewDownloadTask(int id, QString downloadPath);
};
}
#endif // SPKPAGEDOWNLOADS_H

25
inc/page/spkpagehome.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <QVBoxLayout>
#include "page/spkpagebase.h"
#include "ui_homepage.h"
namespace SpkUi
{
class SpkPageHome : public SpkPageBase
{
Q_OBJECT
public:
SpkPageHome(QWidget *parent = nullptr);
~SpkPageHome() { }
Ui::SpkHomepage *ui;
private slots:
void LinkActivated(QString);
private:
void SetupUi();
};
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <QScrollArea>
#include <QVBoxLayout>
#include <QFutureWatcher>
#include <QMutex>
#include "page/spkpagebase.h"
#include "ui_settings.h"
namespace SpkUi
{
class SpkPageSettings : public SpkPageBase
{
Q_OBJECT
public:
SpkPageSettings(QWidget *parent = nullptr);
~SpkPageSettings();
void SetupUi();
void ReadConfiguration();
void SaveConfiguration();
void CountCleaning();
virtual void Activated() override;
private slots:
void on_btnCleanDownloadedContent_clicked();
void on_btnCleanResourceCache_clicked();
void on_btnViewDownloadedContent_clicked();
void on_btnViewResourceCache_clicked();
void CountFinishResource();
void CountFinishDownload();
void CleanedResource();
void CleanedDownload();
private:
QScrollArea *mMainArea;
QVBoxLayout *mMainLay;
QWidget *mSettingsWidget;
Ui::SpkUiSettings *ui;
QString mRepoListUrl;
QFutureWatcher<void> mFwResourceCount,
mFwDownloadCount,
mFwResourceClean,
mFwDownloadClean;
QMutex mMutResource, mMutDownload;
int64_t mBytesResource, mBytesDownloads;
};
}

63
inc/page/spkpageuitest.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <QWidget>
#include <QSplitter>
#include <QPushButton>
#include <QRadioButton>
#include <QCheckBox>
#include <QGroupBox>
#include <QTextEdit>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSlider>
#include <QProgressBar>
#include "spkappitem.h"
#include "spkstretchlayout.h"
#include "page/spkpageappdetails.h"
#include "spkiconbutton.h"
#include "spkloading.h"
namespace SpkUi
{
class SpkPageUiTest : public QSplitter
{
Q_OBJECT
public:
SpkPageUiTest(QWidget *parent = nullptr);
QWidget *WidL, *WidR;
QVBoxLayout *VLayWidgets,
*VLayTestWidgets,
*VLayInput;
QHBoxLayout *HLay4Slider,
*HLayInputBtns;
QTextEdit *TextStylesheet;
QPushButton *BtnApply;
QPushButton *BtnFetch;
QPushButton *Btn;
QCheckBox *Chk;
QRadioButton *Rad;
QGroupBox *Group;
QSlider *SlideH;
QSlider *SlideV;
SpkIconButton *IconBtn;
QProgressBar *Prog;
SpkLoading *Loading;
SpkAppItem *AppItem;
SpkStretchLayout *DetailsLay;
SpkDetailEntry *Detail1, *Detail2, *Detail3;
QWidget *DetailsWidget;
QLineEdit *PopupText;
QPushButton *ShowPopup,
*ShowAbout,
*ShowPkgmgr;
public slots:
void SetStylesheet();
void FetchStylesheet();
void ShowPopupSlot();
};
}

36
inc/pkgs/spkpkgmgrapt.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include "spkpkgmgrbase.h"
#include <QProcess>
class SpkPkgMgrApt final : public SpkPkgMgrBase
{
Q_OBJECT
public:
SpkPkgMgrApt(QObject *parent = nullptr);
static bool DetectRequirements();
virtual PkgInstallResult ExecuteInstallation(QString pkgPath,
int entryId) override;
// APT backend specific
bool ChangeServerRepository(QString content);
private:
void CheckInstallerAvailability();
private slots:
void InstallerExited(int, QProcess::ExitStatus);
private:
QAction *mActAptitudeTerm,
*mActAptTerm,
*mActGdebi,
*mActDeepinPkgInst;
QProcess mInstaller;
};

91
inc/pkgs/spkpkgmgrbase.h Normal file
View File

@@ -0,0 +1,91 @@
#pragma once
#include <QObject>
#include <QMenu>
#include <QAction>
#include <QCursor>
#include <QUrl>
#include <QDesktopServices>
#include "spkutils.h"
class SpkPkgMgrBase : public QObject
{
Q_OBJECT
public:
SpkPkgMgrBase(QObject *parent = nullptr) : QObject(parent)
{
Q_ASSERT(mInstance == nullptr);
mInstance = this;
mActOpen = new QAction(tr("Open package"), this);
mActOpenDir = new QAction(tr("Open containing directory"), this);
mMenu = new QMenu(tr("Package Actions"));
mMenu->addAction(mActOpen);
mMenu->addAction(mActOpenDir);
mMenu->setAttribute(Qt::WA_TranslucentBackground);
mMenu->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
}
enum PkgInstallResult
{
Failed,
Succeeded,
Ignored ///< No installation action is taken, no messages be displayed
};
/**
* @brief Detects if this system qualified to use this kind of packaging
*/
static bool DetectRequirements() { return true; }
/**
* @brief Popup a menu at cursor to select installation methods
* and do installation accordingly
* @param pkgPath Path to the package file
* @param entryId ID of the download entry
*/
virtual PkgInstallResult ExecuteInstallation(QString pkgPath, int entryId)
{
Q_UNUSED(entryId);
auto item = mMenu->exec(QCursor::pos());
if(item == mActOpenDir)
QDesktopServices::openUrl(QUrl(SpkUtils::CutPath(pkgPath)));
else if(item == mActOpen)
QDesktopServices::openUrl(QUrl(pkgPath));
return Ignored;
}
/**
* @brief Called when Spark Store installs a software when it's running
* in CLI mode
* @param pkgPath Path to the package file
*/
virtual PkgInstallResult CliInstall(QString pkgPath)
{
qInfo() << tr("Spark Store cannot install your package because no supported "
"packaging system has been found. You shall decide what you "
"want to do with the downloaded package.\n\n"
"File path:")
<< pkgPath;
return Ignored;
}
public:
static SpkPkgMgrBase *Instance() { return mInstance; }
protected:
QAction *mActOpenDir, *mActOpen, *mActDesc;
QMenu *mMenu;
int mCurrentItemId; ///< ID of currently installing download item
private:
static SpkPkgMgrBase *mInstance;
signals:
void ReportInstallResult(int entryId,
SpkPkgMgrBase::PkgInstallResult result,
int exitCode);
};

View File

@@ -0,0 +1,20 @@
#pragma once
#include "spkpkgmgrbase.h"
class SpkPkgMgrPacman final : public SpkPkgMgrBase
{
Q_OBJECT
public:
SpkPkgMgrPacman(QObject *parent = nullptr);
static bool DetectRequirements();
virtual PkgInstallResult ExecuteInstallation(QString pkgPath,
int entryId) override;
};

87
inc/qt/elidedlabel.h Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
/*
* Slight modifications has been done to the code to make it fit into the project.
*/
#include <QFrame>
#include <QString>
//! [0]
class ElidedLabel : public QFrame
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText)
Q_PROPERTY(bool isElided READ isElided)
public:
explicit ElidedLabel(const QString &text, QWidget *parent = nullptr);
explicit ElidedLabel(QWidget *parent = nullptr);
void setText(const QString &text);
const QString & text() const { return content; }
bool isElided() const { return elided; }
protected:
void paintEvent(QPaintEvent *event) override;
signals:
void elisionChanged(bool elided);
private:
bool elided;
QString content;
};
//! [0]

23
inc/spkabout.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include "spkdialog.h"
class SpkAbout : public SpkDialog
{
Q_OBJECT
public:
SpkAbout(QWidget* parent = nullptr);
~SpkAbout();
static void Show();
private:
QHBoxLayout *mIconLay;
QLabel *mSpkIcon;
QLabel *mSpkVersion;
QLabel *mDescriptionText;
};

41
inc/spkappitem.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "qt/elidedlabel.h"
class SpkAppItem : public QWidget
{
Q_OBJECT
public:
SpkAppItem(int appId, QWidget *parent = nullptr);
void SetIcon(QPixmap p) { mIcon->setPixmap(p); }
void SetTitle(QString s) { mTitle->setText(s); }
void SetDescription(QString s) { mDescription->setText(s); }
protected:
void paintEvent(QPaintEvent *e);
void mousePressEvent(QMouseEvent *e);
void mouseReleaseEvent(QMouseEvent *e);
public:
static constexpr int IconSize = 72;
static constexpr QSize IconSize_ = { IconSize, IconSize };
private:
QLabel *mIcon;
QLabel *mTitle;
ElidedLabel *mDescription;
int mAppId;
bool mPressCond;
QVBoxLayout *mLayText;
QHBoxLayout *mMainLay;
signals:
void clicked(int);
};

51
inc/spkconfig.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef SPKCONFIG_H
#define SPKCONFIG_H
#include <QSettings>
#include <QHash>
#include <QPair>
#include <functional>
class SpkConfig : public QObject
{
Q_OBJECT
public:
SpkConfig(QObject *parent, QString configPath);
~SpkConfig();
/**
* @brief BindField If a variable is bound to the specified key, then future chanegs via SetField
* will modify the provided variable. A callback can also be specified to make
* sure the chanegs are acceptable.
* @param key
* @param value A pointer to the variable to be bound
* @param defaultValue
* @param callback When SetField is called to modify this specific key associated with a callback,
* the callback is called. If the callback returned false then the original value
* is restored to the value target, and changes won't be saved in mSettings, and
* SetField will return false too. It is used to ensure if the target can accept
* the changes.
* @return false when the key is already bound.
*/
bool BindField(QString key, QString* value, QString defaultValue, std::function<bool(void)> callback = nullptr);
bool BindField(QString key, int* value, int defaultValue, std::function<bool(void)> callback = nullptr);
bool SetField(QString key, QString value);
bool SetField(QString key, int value);
// Wrapper of QSettings::value, used for "read once on startup" configurations
QVariant ReadField(QString key, QVariant defaultValue);
// Wrapper of QSettings::setValue, used for "set and restart to take effect" configurations
void SetSettings(QString key, QVariant value);
// Wrapper of QSettings::sync
void Sync();
private:
QSettings mSettings;
QHash<QString, QPair<QString*, std::function<bool(void)>>> mStringBindMap;
QHash<QString, QPair<int*,std::function<bool(void)>>> mIntBindMap;
};
#endif // SPKCONFIG_H

40
inc/spkdialog.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <QList>
#include <QButtonGroup>
#include "spkui_general.h"
#include "spktitlebar.h"
#include "spkwindow.h"
class SpkDialog : public SpkWindow
{
Q_OBJECT
friend class SpkMsgBox;
public:
SpkDialog(QWidget *parent = nullptr);
~SpkDialog();
void AddButton(QString text, SpkUi::SpkButtonStyle style = SpkUi::SpkButtonStyle::Normal);
void AddWidget(QWidget*);
void AddLayout(QLayout*);
void AddSpacing(int);
void AddStretch(int a = 0);
void SetMargin(int);
void SetMargin(int, int, int, int);
int Exec();
private slots:
void ButtonPressed(int);
void ForceClose();
signals:
void ExitEventLoop(int);
void CloseWindow();
protected:
QWidget *mDialogWidget;
private:
QVBoxLayout *mMainVLay, *mWidgetsVLay;
QHBoxLayout *mBtnLay;
QButtonGroup *mBtnGroup;
QList<QObject*> mWidgetsList, mParentsList;
};

125
inc/spkdownload.h Normal file
View File

@@ -0,0 +1,125 @@
#pragma once
#include "spkstore.h"
#include <QTimer>
/**
* @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.
*
* Because of this, SpkDownloadMgr does not support complex download queues, cannot handle
* pauses, and can only work on a sequential list of tasks.
*/
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
FailCancel, ///< User has cancelled the task
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.
*
* Each worker has a watch dog value, incremented each time the download speed is
* updated, and zeroed each time the worker has data ready. If the value exceeds a
* preset maximum, then this worker is considered timed out and killed.
*/
struct DownloadWorker
{
QNetworkReply *Reply; ///< Reply from the network
int Watchdog; ///< Watch dog value watching for a timed out worker
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
};
constexpr static int WatchDogMaximum = 7;
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);
QString GetDestFilePath(QString downloadPath);
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;
QTimer mProgressEmitterTimer;
qint64 mDownloadedBytes;
int mCurrentDownloadId; ///< Indicates download status. -1 means no download going on.
int mActiveWorkerCount;
QString mBulkServerPaths; ///< Config string, modification are taken care of by callback.
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***
void WorkerError(QNetworkReply::NetworkError);
void ProgressTimer();
private:
void ProcessWorkerError(DownloadWorker &, int id);
void LinkReplyWithMe(QNetworkReply*);
void TryScheduleFailureRetries();
void TryScheduleFailureRetries(int i); ///< Try schedule on a specific task slot.
bool ServerAddressesChangedCallback(); ///< Called by SpkConfig upon address changing
signals:
void DownloadProgressed(qint64 bytes, qint64 total, int id);
void DownloadStopped(TaskResult status, int id);
};

78
inc/spkdownloadentry.h Normal file
View File

@@ -0,0 +1,78 @@
#pragma once
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include "qt/elidedlabel.h"
#include "spkloading.h"
#include <QProgressBar>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QTime>
class SpkDownloadEntry : public QWidget
{
Q_OBJECT
public:
explicit SpkDownloadEntry(QWidget* parent = nullptr);
~SpkDownloadEntry();
static constexpr QSize IconSize { 64, 64 };
enum DownloadEntryStatus
{
Invalid = -1,
Waiting,
Starting,
Downloading,
DownloadFailed,
ToBeInstalled,
Installing,
Installed,
InstallFailed
};
void SetTotalBytes(qint64 total);
void SetBasicInfo(QString name, QPixmap icon, QString filePath);
void SetStatus(DownloadEntryStatus status, QString msg = "");
void Progress(qint64 bytes);
QString GetTaskName() { return mAppName->text(); }
QString GetFilePath() { return mFilePath; }
enum EntryAction
{
AbortDownload,
RetryDownload,
StartInstall,
RemoveEntry
};
private slots:
void ActionButton();
void DeleteButton();
private:
QLabel *mIcon, *mMessage;
ElidedLabel *mAppName;
QProgressBar *mProgress;
QPushButton *mBtnDelete,
*mBtnActions; // Actions include Retry Pause Install etc, one status at a time
SpkLoading *mLoading;
QHBoxLayout *mLayMsgs, *mLayMain;
QVBoxLayout *mLayInfo;
// Download status data
qint64 mTotalBytes, mDownloadedBytes;
QTime mLastReportTime;
QString mReadableTotalSize;
QString mFilePath;
DownloadEntryStatus mStatus;
signals:
void Action(EntryAction);
};

4
inc/spkfillwidget.h Normal file
View File

@@ -0,0 +1,4 @@
#ifndef SPKFILLWIDGET_H
#define SPKFILLWIDGET_H
#endif // SPKFILLWIDGET_H

19
inc/spkfocuslineedit.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <QtWidgets/QLineEdit>
class SpkFocusLineEdit final : public QLineEdit
{
Q_OBJECT
public:
explicit SpkFocusLineEdit(QWidget *parent = nullptr) : QLineEdit(parent) {}
protected:
void focusInEvent(QFocusEvent *e) override { emit focusGained(); }
void focusOutEvent(QFocusEvent *e) override { emit focusLost(); }
signals:
void focusGained();
void focusLost();
};

30
inc/spkiconbutton.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef SPKICONBUTTON_H
#define SPKICONBUTTON_H
#include <QPushButton>
#include "spkui_general.h"
class SpkIconButton : public QPushButton
{
Q_OBJECT
public:
SpkIconButton(QWidget *parent = nullptr);
~SpkIconButton() {};
void SetIcon(QIcon, QSize);
void SetIcon(QPixmap);
void SetIconSize(QSize);
protected:
void paintEvent(QPaintEvent *) override;
private:
static enum { Dark, Light } sCurrentTheme;
QPixmap mPmapPaintedIcon;
QSize mPmapSize;
static constexpr int IconMargin = 8; // shall we make it changable?
};
#endif // SPKICONBUTTON_H

61
inc/spkimgviewer.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include <QPainter>
#include <QScrollArea>
#include "spkwindow.h"
class ImgView : public QWidget
{
Q_OBJECT
public:
ImgView(QWidget *parent = nullptr) : QWidget(parent) { mPixmap = nullptr; }
void SetPixmap(QPixmap *p) { mPixmap = p; if(p) setFixedSize(p->size()); update(); }
protected:
void paintEvent(QPaintEvent *e)
{
QPainter p(this);
if(mPixmap)
p.drawPixmap(0, 0, *mPixmap);
p.end();
e->accept();
}
private:
QPixmap *mPixmap;
};
class SpkImgViewer : public SpkWindow
{
Q_OBJECT
public:
SpkImgViewer(QWidget *parent = nullptr);
void ShowWithImage(int idx);
void SetImageTotal(int a) { mTotalImg = a; }
public slots:
void Clear();
void SetPixmap(int idx, QPixmap *img);
private slots:
void SwitchToImage(int idx);
protected:
bool event(QEvent*) override;
private:
void ResizeToFitImageSize(QSize);
private:
QPushButton *mBtnPrev, *mBtnNext;
QScrollArea *mImgArea;
QLabel *mImgIndict;
QMap<int, QPixmap*> mImgMap;
int mCurrentImg, mTotalImg;
QPixmap mIconLoading;
ImgView *mImgShow;
};

25
inc/spkjsonapiconsumer.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef SPKJSONAPICONSUMER_H
#define SPKJSONAPICONSUMER_H
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QtNetwork/QNetworkAccessManager>
class SpkJsonApiConsumer: public QObject
{
Q_OBJECT
public:
SpkJsonApiConsumer();
private:
QNetworkAccessManager mNet;
public slots:
void RequestUrl(QUrl);
signals:
void RequestFinish(QJsonDocument, int);
};
#endif // SPKJSONAPICONSUMER_H

34
inc/spkloading.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include <QFrame>
#include <QTimeLine>
class SpkLoading : public QFrame
{
Q_OBJECT
public:
explicit SpkLoading(QWidget *parent = nullptr);
virtual void paintEvent(QPaintEvent *e) override;
virtual void resizeEvent(QResizeEvent *e) override;
private:
QTimeLine *mAnimTimer;
QList<int> mSizeList;
int mUserHeight = 0;
int dw, dh;
double dx, dy;
public slots:
void start() { mAnimTimer->start(); }
void stop() { mAnimTimer->stop(); }
void Begin() { start(); setVisible(true); }
void End() { stop(); setVisible(false); }
void setHeight(int h) { mUserHeight = h; }
void reset();
private slots:
void timer(int s);
void loop();
};

44
inc/spklogging.h Normal file
View File

@@ -0,0 +1,44 @@
/**
* @brief Simple logging for Spark Store
*/
#pragma once
#pragma push_macro("signals")
#undef signals
#include <libnotify/notify.h>
#define signals DUMMY
#pragma pop_macro("signals")
#include <QString>
#include <QStringListModel>
#include <QTextStream>
#include <QFile>
class SpkLogger
{
QStringListModel mLogData;
QString mLogPath;
QFile mLogFile;
QTextStream mLogWriter;
static SpkLogger *Instance;
void Initialize(QString suggestPath = "");
bool NotifyViable = false;
public:
SpkLogger(QString suggestPath = "");
~SpkLogger();
static SpkLogger *GetInstance() { return Instance; };
void Log(QString message);
void Warning(QString message);
void Error(QString message, bool pop = false);
void Critical(QString message);
void Notify(QString);
};
#define sLog(X) SpkLogger::GetInstance()->Log(X)
#define sWarn(X) SpkLogger::GetInstance()->Warning(X)
#define sErr(X) SpkLogger::GetInstance()->Error(X)
#define sErrPop(X) SpkLogger::GetInstance()->Error(X,true)
#define sCritical(X) SpkLogger::GetInstance()->Critical(X)
#define sNotify(X) SpkLogger::GetInstance()->Notify(X)

236
inc/spkmainwindow.h Normal file
View File

@@ -0,0 +1,236 @@
//
// Created by rigoligo on 2021/5/9.
//
#pragma once
#include "spkwindow.h"
#include <vector>
#include <QTextEdit>
#include <QStackedWidget>
#include <QButtonGroup>
#include <QJsonObject>
#include "spksidebartree.h" // In place of #include <QTreeWidget>
#include <QPointer>
#include <QTimeLine>
#include <QQueue>
#include "spkfocuslineedit.h"
#include "spkiconbutton.h"
#include "page/spkpageuitest.h"
#include "page/spkpagehome.h"
#include "page/spkpageapplist.h"
#include "page/spkpageappdetails.h"
#include "page/spkpagedownloads.h"
#include "page/spkpagesettings.h"
class QNetworkReply;
namespace SpkUi
{
enum SpkStackedPages
{
PgInvalid = -1,
PgHomepage,
PgAppList,
PgAppDetails,
PgDownloads,
PgSettings,
PgQssTest // Must be at last
};
const std::vector<SpkStackedPages> ResourceContexts
{
PgAppList
};
class SpkSidebarSelector : public QObject
{
Q_OBJECT
private:
QPushButton *mLastCheckedBtn;
QTreeWidgetItem *mLastSelectedItem;
QTreeWidget *mCategoryWidget;
QVector<QTreeWidgetItem *> mUnusableItems; // Unselectable top level items; never changes
QTreeWidgetItem* mLastCategoryItem;
int mLastCategoryPage;
public:
SpkSidebarSelector(QObject *parent = nullptr) : QObject(parent)
{
mLastCheckedBtn = nullptr;
mLastSelectedItem = nullptr;
}
// Tree item can either represent a page or a category, data of special roles
// help identify them; Buttons instead can only represent a page
constexpr static int RoleItemIsCategory = Qt::UserRole + 1;
constexpr static int RoleItemCategoryPageId= Qt::UserRole + 2;
void BindPageSwitcherButton(QAbstractButton* w)
{
connect(w, &QAbstractButton::toggled,
this, &SpkSidebarSelector::ButtonToggled);
connect(w, &QAbstractButton::pressed,
this, &SpkSidebarSelector::ButtonPressed);
}
void BindCategoryWidget(QTreeWidget* w)
{
mCategoryWidget = w;
connect(w, &QTreeWidget::itemPressed, this,
&SpkSidebarSelector::TreeItemSelected);
}
void AddUnusableItem(QTreeWidgetItem *i) { mUnusableItems.append(i); }
void GoBack()
{
emit SwitchToCategory(mLastCategoryPage, 0);
mCategoryWidget->currentItem()->setSelected(false);
mLastCategoryItem->setSelected(true);
}
private slots:
// We assume the objects in interest all have the correct properties
void ButtonToggled(bool aBtnState)
{
auto b = qobject_cast<QPushButton*>(sender());
if(mLastCheckedBtn)
{
if(mLastCheckedBtn != b)
{
mLastCheckedBtn->setChecked(false);
mLastCheckedBtn = nullptr;
}
}
else if(mLastSelectedItem)
{
mLastSelectedItem->setSelected(false);
mLastSelectedItem = nullptr;
}
mLastCheckedBtn = b;
auto id = b->property("spk_pageno").toInt();
emit SwitchToPage(static_cast<SpkStackedPages>(id));
}
void ButtonPressed()
{ // Prevent a selected button from being deselected by clicking on it
auto b = qobject_cast<QPushButton*>(sender());
if(mLastCheckedBtn == b)
b->setChecked(false);
}
void TreeItemSelected(QTreeWidgetItem *item, int column)
{
if(mUnusableItems.contains(item))
{
UnusableItemSelected(item); return;
}
if(mLastCheckedBtn)
{
mLastCheckedBtn->setChecked(false);
mLastCheckedBtn = nullptr;
}
mLastSelectedItem = item;
auto id = item->data(column, RoleItemCategoryPageId).toInt();
if(item->data(column, RoleItemIsCategory).toBool())
emit SwitchToCategory(id, 0), mLastCategoryPage = id, mLastCategoryItem = item;
else
emit SwitchToPage(static_cast<SpkStackedPages>(id));
}
void UnusableItemSelected(QTreeWidgetItem *i)
{
i->setSelected(false);
if(mLastSelectedItem)
{
mLastSelectedItem->setSelected(true);
}
else if(mLastCheckedBtn)
{
mLastCheckedBtn->setChecked(true);
}
}
signals:
void SwitchToCategory(int aCategoryId, int aPage);
void SwitchToPage(SpkStackedPages aPageId);
};
class SpkMainWidget : public QFrame
{
Q_OBJECT
public:
SpkMainWidget(QWidget *parent = nullptr);
QHBoxLayout *HorizontalDivide;
QStackedWidget *Pager;
// Category widget is for switching pages
SpkIconButton *BtnSettings, *BtnFeedback, *BtnLogs, *BtnDayNight, *BtnBack;
SpkSidebarTree *CategoryWidget;
QMap<int, QTreeWidgetItem> *CategoryItemMap;
SpkSidebarSelector *SidebarMgr;
QTreeWidgetItem *HomepageItem,
*CategoryParentItem,
*AppDetailsItem,
*DownloadsItem,
*UiTestItem;
// Title bar search bar
SpkFocusLineEdit *SearchEdit;
QAction *ActClearSearchBar, *ActSearchIcon;
QTimeLine *SearchBarAnim;
//Pages
SpkPageUiTest *PageQssTest;
SpkPageHome *PageHome;
SpkPageAppList *PageAppList;
SpkPageAppDetails *PageAppDetails;
SpkPageDownloads *PageDownloads;
SpkPageSettings *PageSettings;
};
}
class SpkMainWindow : public SpkWindow
{
Q_OBJECT
private:
SpkUi::SpkMainWidget *ui;
public:
SpkMainWindow(QWidget *parent = nullptr);
void PopulateCategories(QJsonArray);
private:
void Initialize();
private:
QPointer<QNetworkReply> mCategoryGetReply,
mCategoryAppListGetReply,
mAppDetailsGetReply;
SpkUi::SpkStackedPages mCurrentPage = SpkUi::PgInvalid;
QList<QPair<SpkIconButton*, QString>> mThemedUiIconReferences;
public slots:
void ReloadThemedUiIcons();
void RefreshCategoryData();
private slots:
void SwitchDayNightTheme();
void SwitchToPage(SpkUi::SpkStackedPages page);
void CategoryDataReceived();
// Enter a category (and switch pages)
void EnterCategoryList(int aCategoryId, int aPage);
void CategoryListDataReceived();
// Search a keyword (and switch pages)
void SearchKeyword(QString aKeyword, int aPage);
void SearchDataReceived();
// Enter the details page of an application (and switch pages)
void EnterAppDetails(int aAppId);
void AppDetailsDataReceived();
private:
void PopulateAppList(QJsonObject appData, QString &&keyword);
void PopulateAppDetails(QJsonObject appDetails);
};

20
inc/spkmsgbox.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <QStyle>
#include <QMessageBox>
#include "spkdialog.h"
class SpkMsgBox : public SpkDialog
{
Q_OBJECT
public:
SpkMsgBox(QWidget *parent = nullptr);
static int StaticExec(QString msg, QString title, QMessageBox::Icon = QMessageBox::NoIcon,
QMessageBox::StandardButtons = QMessageBox::Ok, QString extra = "",
bool expanded = false);
private:
static void AddButtons(SpkMsgBox *me, QMessageBox::StandardButtons b);
QList<QMessageBox::StandardButton> mButtonList;
static constexpr QSize IconSize {48, 48};
static constexpr int Margin = 10;
};

17
inc/spknotifydot.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <QLabel>
class SpkNotifyDot : public QLabel
{
Q_OBJECT
public:
SpkNotifyDot(QWidget *parent = nullptr);
~SpkNotifyDot() {}
protected:
virtual void paintEvent(QPaintEvent *) override;
// virtual int widthForHeight(int h) override;
};

30
inc/spkpopup.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <QLabel>
#include <QWidget>
#include <QHBoxLayout>
#include <QPropertyAnimation>
#include <QSequentialAnimationGroup>
class SpkMainWindow;
namespace SpkUi
{
class SpkPopup : public QWidget
{
Q_OBJECT
public:
SpkPopup(QWidget *parent, int aMillis = 3000);
public slots:
void Show(QString aText);
private:
QPoint MoveOffset;
QLabel *mText;
QHBoxLayout *mBox;
QPropertyAnimation *mAnimFadeIn, *mAnimFadeOut;
QSequentialAnimationGroup *mAnim;
};
}

51
inc/spkqsshelper.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <map>
#include <list>
#include <QColor>
namespace SpkUi
{
QColor ColorTextOnBackground(QColor);
namespace Qss
{
enum ColorSetIndex
{
GlobalBgnd = 0,
ControlsBgnd,
ControlsBgndHighlighted,
SelectionBgnd, AccentColor = SelectionBgnd,
SelectionBgndHighlighted, AccentColorHighlighted = SelectionBgndHighlighted,
LightCtrlsGradLight,
LightCtrlsGradDark,
LightCtrlsGradDarker,
LightCtrlsDisabledBackground,
DarkCtrlsGradLight,
DarkCtrlsGradDark,
DarkCtrlsGradDarker,
DarkCtrlsDisabledBackground,
TextOnSelection, TextOnAccentColor = TextOnSelection,
TextOnGlobalBgnd,
TextOnControlsBgnd,
TextLighter,
TextEvenLighter,
TextDisabled,
GlossyEdge,
ShadesEdge,
ScrollBarNorm,
ScrollBarHover,
DivideLine,
};
extern const std::list<ColorSetIndex> AccentColorExceptions;
extern const std::map<ColorSetIndex, const char *> ColorSet2Token;
extern const std::map<ColorSetIndex, QColor>
DarkColorSet, LightColorSet;
using ColorSet = std::map<Qss::ColorSetIndex, QColor>;
}
}

101
inc/spkresource.h Normal file
View File

@@ -0,0 +1,101 @@
#pragma once
#include <QObject>
#include <QQueue>
#include <QSemaphore>
#include <QMap>
#include <QNetworkReply>
#include <QVariant>
struct ResourceResult;
class SpkResourceContext;
class SpkPageBase;
class SpkResource : public QObject
{
Q_OBJECT
public:
enum class ResourceType { HomeImage, AppIcon, TagIcon, AppScreenshot, };
enum class ResourceStatus
{
Ready, ///< The resource is ready to be read now and the file link is included
Deferred, ///< The resource is not in cache or outdated, and is being downloaded
Failed, ///< The requested resource can not be downloaded and thus can not be loaded.
};
private:
struct ResourceTask
{
QString pkgName, path;
ResourceType type;
QVariant info;
int id;
};
static const QMap<ResourceType, QString> ResourceName;
public:
SpkResource(QObject *parent = nullptr);
ResourceResult
RequestResource(const int aId, const QString &aPkgName, ResourceType aType, const QString &aPath,
const QVariant &aInfo = 0);
void PurgeCachedResource(const QString &aPkgName, SpkResource::ResourceType aType,
const QVariant &aInfo);
inline QString GetCachePath(const ResourceTask &task);
ResourceResult CacheLookup(QString pkgName, ResourceType type, QVariant info);
/**
* @brief When the resource context was changed, the new context needs to acquire the resource
* manager, so the resource manager can download resource for the new context.
*
* A resource context, in Spark Store, is likely to be a "page" of the UI. AppList is
* a page, Homepage is a page, AppDetails is a page. But each category is not a separated
* context; they're all from AppList page.
* @param dest Pass a SpkResourceContext as the resource acquisition destination. Signals will
* be connected in this function internally. When a resource was downloaded, this
* object will be notified.
* @param clearQueue determines if the awaiting tasks needs to be cleared.
* @param stopOngoing determines if ongoing tasks needs to be terminated.
*/
void Acquire(SpkPageBase *dest, bool stopOngoing, bool clearQueue = true);
public:
static SpkResource* Instance;
const int mMaximumConcurrent; ///< Maximum number of concurrent resource downloads
const QString mCacheDirectory;///< Where caches were stored
private slots:
void ResourceDownloaded();
void TryBeginAwaitingTasks();
private:
ResourceResult LocateCachedResource(const ResourceTask &task);
signals:
void AcquisitionFinish(int id, ResourceResult result);
private:
// Operations to mAwaitingRequests and mWorkingRequests must be all made synchronously.
// When connecting signal/slot pairs regarding operations to them, use QueuedConnection.
QQueue<ResourceTask> mAwaitingRequests;
QSemaphore *mRequestSemaphore;
QMap<QNetworkReply*, int> mWorkingRequests;
};
struct ResourceResult
{
SpkResource::ResourceStatus status;
QByteArray data;
};
Q_DECLARE_METATYPE(ResourceResult);
/**
* @brief SpkResourceContext is meant to be used by connecting signals from it. An object inheriting
* it would be theoretically a resource context. A resource context can acquire a SpkResource
* management object for downloading resources. More details at SpkResource::Acquire.
*/

17
inc/spksidebartree.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <QTreeWidget>
namespace SpkUi
{
class SpkSidebarTree : public QTreeWidget
{
Q_OBJECT
public:
SpkSidebarTree(QWidget* parent = nullptr);
protected:
void mouseMoveEvent(QMouseEvent *) override;
void mousePressEvent(QMouseEvent *) override;
};
}

51
inc/spkstore.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <QJsonDocument>
#include <QString>
#include <QSettings>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkAccessManager>
#include "spklogging.h"
#include "spkresource.h"
#include "spkconfig.h"
class SpkMainWindow;
/**
* @brief SpkStore class is the core of the store client side program, it is constructed first and
* handling all processing after the launch. All client side data should be held by it,
* including preferences class, category names, request URLs, downloads status, packaging
* backend interfaces and so on.
*/
class SpkStore : public QObject
{
Q_OBJECT
public:
static SpkStore *Instance;
SpkConfig *mCfg;
SpkStore(bool aCli, QString &aLogPath);
~SpkStore();
SpkMainWindow* GetRootWindow() { return mMainWindow; }
void SetApiRequestUrl(QString aUrlStr) { mApiRequestUrl = aUrlStr; }
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;
SpkMainWindow *mMainWindow = nullptr;
SpkResource *mResMgr = nullptr;
QNetworkAccessManager *mNetMgr = nullptr;
QString mDistroName,
mApiRequestUrl,
mResourceRequestUrl,
mUserAgentStr,
mConfigPath;
};

27
inc/spkstretchlayout.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QtWidgets>
#include <QVector>
/**
* @brief SpkStretchLayout is meant solely for use with app list, where each layout item is
* considered the same size.
*/
class SpkStretchLayout : public QLayout
{
public:
SpkStretchLayout(QWidget *parent = nullptr);
~SpkStretchLayout();
void addItem(QLayoutItem *item) override;
QSize sizeHint() const override;
QSize minimumSize() const override;
int count() const override;
QLayoutItem* itemAt(int) const override;
QLayoutItem* takeAt(int) override;
void setGeometry(const QRect &rect) override;
private:
QVector<QLayoutItem*> mItems;
};

71
inc/spktitlebar.h Normal file
View File

@@ -0,0 +1,71 @@
//
// Created by rigoligo on 2021/5/8.
//
#pragma once
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QPainter>
#include <QBrush>
#include <QPushButton>
#include <QMainWindow>
namespace SpkUi
{
}
class SpkWindow;
class SpkTitleBarDefaultButton : public QPushButton
{
Q_OBJECT
public:
SpkTitleBarDefaultButton(QWidget* parent = nullptr);
enum OperationButton { Minimize = 1, MaximizeRestore = 2, Close = 4, Restore = 3 };
void SetRole(OperationButton);
constexpr static int ButtonWidth = 60;
protected:
void paintEvent(QPaintEvent *e) override;
private:
OperationButton Role;
void PaintSymbol(QPainter &);
};
class SpkTitleBar : public QFrame
{
Q_OBJECT
friend class SpkWindow;
public:
SpkTitleBar(QWidget *parent = nullptr);
~SpkTitleBar();
static constexpr int Height = 48;
using OperationButton = SpkTitleBarDefaultButton::OperationButton;
void SetOperationButton(OperationButton);
void SetIcon(QPixmap p) { mIcon->setPixmap(p); }
void SetTitle(QString t) { mTitle->setText(t); }
QString GetTitle() { return mTitle->text(); }
void SetUseIcon(bool t) { mIcon->setVisible(t); }
void SetLinkedWindow(SpkWindow *w) { mLinkedWindow = w; }
QHBoxLayout *GetUserSpace() { return mUserSpace; }
protected:
bool event(QEvent*) override;
private:
QLabel *mIcon;
QLabel *mTitle;
SpkWindow *mLinkedWindow;
QHBoxLayout *mMainLayout, *mUserSpace, *mBtnGroup;
SpkTitleBarDefaultButton *mBtnMin, *mBtnMaxRestore, *mBtnClose;
private slots:
void CloseWindow();
void MinimizeWindow();
void MaximizeRestoreWindow();
};

81
inc/spkui_general.h Normal file
View File

@@ -0,0 +1,81 @@
//
// Created by rigoligo on 2021/5/8.
//
#pragma once
#include <QWidget>
#include <QString>
#include <QSize>
#include <QColor>
#include <map>
#include "spkqsshelper.h"
#include "dtk/spkdtkplugin.h"
namespace SpkUi
{
enum SpkUiStyle { Invalid, Light, Dark };
enum SpkButtonStyle { Normal = 0, Recommend, Warn };
class SpkPopup;
constexpr int StackTraceArraySize = 64;
constexpr const char * const StoreIconName = "spark-store";
class UiMetaObject : public QObject
{
Q_OBJECT
private:
static UiMetaObject *sGlobalInstance;
public:
UiMetaObject() {}
public slots:
void SetAccentColor(QColor);
void SetDarkLightTheme(bool isDark);
signals:
void SetThemeButtonVisible(bool);
};
extern UiMetaObject SpkUiMetaObject;
extern SpkUiStyle CurrentStyle;
extern QString StylesheetBase, CurrentStylesheet;
extern QColor ColorLine, ColorBack,
ColorBtnMaskUnselected, ///< SpkIconButton icon mask colors, unselected & selected
ColorBtnMaskSelected;
extern QSize PrimaryScreenSize;
extern SpkDtkPlugin *DtkPlugin;
extern QStyle *OldSystemStyle;
extern std::map<Qss::ColorSetIndex, QColor> CurrentColorSet;
extern SpkPopup *Popup;
namespace States
{
extern bool IsDDE, IsUsingDtkPlugin;
extern bool DoRespondAutoTheme;
extern int LightDarkMode; ///< Tied to settings UI value
bool ThemeConfigCallback();
}
namespace Priv
{
extern bool CrashHandlerActivated;
using namespace SpkUi::Qss;
}
void Initialize();
void GuessAppropriateTheme();
void PrepareForDeepinDesktop();
bool CheckIsDeepinDesktop();
QString StylesheetFromColors(Qss::ColorSet);
QIcon GetThemedIcon(QString);
void CrashSignalHandler(int);
void SetGlobalStyle(const SpkUiStyle, const bool aPreserveAccentColor);
};

32
inc/spkuimsg.h Normal file
View File

@@ -0,0 +1,32 @@
//
// Created by rigoligo on 2021/5/9.
//
#pragma once
#pragma push_macro("signals")
#undef signals
#include <libnotify/notify.h>
#define signals DUMMY
#pragma pop_macro("signals")
#include <QString>
#include <QObject>
#include "spkui_general.h"
class SpkUiMessage : public QObject
{
Q_OBJECT
public:
static void SendDesktopNotification(QString aMsg,
const char * const aIcon = SpkUi::StoreIconName);
static void SendStoreNotification(QString);
static void SetDesktopNotifyTimeout(int ms);
private:
static NotifyNotification *_notify;
static int mTimeoutDesktop;
signals:
};

36
inc/spkutils.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <QPointer>
#include <QString>
#include <QSettings>
#include <QFile>
#include <QtNetwork/QNetworkReply>
#include "spkstore.h"
#include "spklogging.h"
#define STORE (SpkStore::Instance)
#define CFG (SpkStore::Instance->mCfg)
#define RES (SpkResource::Instance)
#define PKG (SpkPkgMgrBase::Instance())
namespace SpkUtils
{
QString GetDistroName();
void VerifySingleRequest(QPointer<QNetworkReply> aReply);
void DeleteReplyLater(QNetworkReply *aReply);
int VerifyReplyJson(QNetworkReply *aReply, QJsonValue& aRetDoc);
QString CutFileName(QString);
QString CutPath(QString);
QString BytesToSize(size_t s, int prec = 2);
bool EnsureDirExists(QString path);
bool FindViableTerminal();
extern QPair<QString, QString> AvailableTerminal;
}

67
inc/spkwindow.h Normal file
View File

@@ -0,0 +1,67 @@
//
// Created by rigoligo on 2021/5/8.
//
#pragma once
#include <QFrame>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QCloseEvent>
class SpkTitleBar;
class SpkWindow : public QWidget
{
Q_OBJECT
public:
enum SpkWindowStyle { Dark, Light };
static constexpr int BorderWidth = 7;
private:
QWidget *mUserCentralWidget;
QVBoxLayout *mMainVLayout;
SpkTitleBar *mTitleBarComponent;
int mCornerRadius;
bool mMoving, mResizing, mMaximized, mResizable;
Qt::Edges mEdgesBeingResized;
QPoint mMoveOffset;
bool (*mCloseHook)(void);
bool mUseCustomEvents;
public:
SpkWindow(QWidget *parent = nullptr);
~SpkWindow() override;
void SetCentralWidget(QWidget *);
bool GetUseTitleBar();
bool GetResizable() { return mResizable; }
void SetCloseHook(bool(*f)(void));
public slots:
void SetCornerRadius(int);
void SetUseTitleBar(bool);
void SetResizable(bool a) { mResizable = a; }
void SetCentralMargin(int, int, int, int);
void ClearCloseHook();
void RecalculateSize();
SpkTitleBar *GetTitleBar();
SpkTitleBar *SetTitleBar(SpkTitleBar*, bool replace = false);
signals:
void Closed();
protected:
bool event(QEvent *) override;
Qt::Edges DetectEdgeOnThis(QPoint);
void SetMouseCursor(Qt::Edges);
void ResizeWindowByCursor(QPoint);
void closeEvent(QCloseEvent *) override;
void paintEvent(QPaintEvent *) override;
private:
void PopulateUi();
void paintWindowBorder();
};

20
inc/telemetry/collectid.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <QObject>
#include <QFile>
namespace SpkTelemetry
{
void FunCollectId()
{
#if !defined(NDEBUG) || !defined(SPARK_FORCE_TELEMETRY) // Debug builds shouldn't transmit telemetry data unless asked for
return;
#else
QFile idFile("/etc/machine-id");
if(!idFile.open(QFile::ReadOnly))
return;
auto machineId = idFile.readAll();
#endif
}
}

1077
lang/zh.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
#include "mainwindow.h"
#include <DApplication>
#include <DWidgetUtil> //Dtk::Widget::moveToCenter(&w); 要调用它就得引用DWidgetUtil
DWIDGET_USE_NAMESPACE
int main(int argc, char *argv[])
{
DApplication::loadDXcbPlugin(); //让bar处在标题栏中
DApplication a(argc, argv);
a.setAttribute(Qt::AA_UseHighDpiPixmaps);
a.loadTranslator();
a.setOrganizationName("deepin");
a.setApplicationVersion(DApplication::buildVersion("1.0"));
a.setApplicationAcknowledgementPage("https://gitee.com/deepin-community-store/deepin-community-store");
a.setProductIcon(QIcon::fromTheme("deepin-community-store")); //设置Logo
a.setProductName("深度社区应用商店");
a.setApplicationName("社区应用商店"); //只有在这儿修改窗口标题才有效
MainWindow w;
w.show();
//让打开时界面显示在正中
Dtk::Widget::moveToCenter(&w);
return a.exec();
}

View File

@@ -1,20 +0,0 @@
#include "mainwindow.h"
#include <DMainWindow>
DWIDGET_USE_NAMESPACE
MainWindow::MainWindow(QWidget *parent)
: DMainWindow(parent)
{
w = new Widget;
resize(w->size()); //设置窗口大小
setMinimumSize(900,750);
setCentralWidget(w);
}
MainWindow::~MainWindow()
{
}

View File

@@ -1,21 +0,0 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <DMainWindow>
#include "widget.h"
DWIDGET_USE_NAMESPACE
class MainWindow : public DMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Widget *w;
};
#endif // MAINWINDOW_H

View File

@@ -0,0 +1,14 @@
set(PLUGIN_SOURCE
spkdtkplugin.h
spkdtkplugin_impl.h
spkdtkplugin.cpp)
find_package(PkgConfig REQUIRED)
pkg_check_modules(dtkwidget REQUIRED IMPORTED_TARGET dtkwidget)
pkg_check_modules(dtkgui REQUIRED IMPORTED_TARGET dtkgui)
pkg_check_modules(dtkcore REQUIRED IMPORTED_TARGET dtkcore)
add_library(spkdtkplugin SHARED ${PLUGIN_SOURCE})
target_link_libraries(spkdtkplugin PkgConfig::dtkwidget PkgConfig::dtkgui PkgConfig::dtkcore ${REQUIRED_LIBS_QUALIFIED})
install(TARGETS spkdtkplugin LIBRARY DESTINATION lib)

View File

@@ -0,0 +1,36 @@
#include <DGuiApplicationHelper>
#include <DPlatformWindowHandle>
#include <DPlatformTheme>
#include "spkdtkplugin_impl.h"
using Dtk::Widget::DPlatformWindowHandle;
void SpkDtkPluginImpl::Initialize()
{
connect(DGuiApplicationHelper::instance()->systemTheme(),
&DPlatformTheme::activeColorChanged,
this,
&SpkDtkPluginImpl::AccentColorChanged);
connect(DGuiApplicationHelper::instance(),
&DGuiApplicationHelper::themeTypeChanged,
[&](Dtk::Gui::DGuiApplicationHelper::ColorType t)
{
this->DarkLightThemeChanged(t == Dtk::Gui::DGuiApplicationHelper::DarkType);
});
}
void SpkDtkPluginImpl::addWindow(QWidget *w, QObject *parent)
{
DPlatformWindowHandle *h = new DPlatformWindowHandle(w, parent);
Q_UNUSED(h);
}
QColor SpkDtkPluginImpl::GetAccentColor()
{
return DGuiApplicationHelper::instance()->systemTheme()->activeColor();
}
bool SpkDtkPluginImpl::GetIsDarkTheme()
{
return DGuiApplicationHelper::instance()->themeType() == DGuiApplicationHelper::DarkType;
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <QWidget>
class SpkDtkPlugin : public QObject
{
Q_OBJECT
public:
virtual ~SpkDtkPlugin() = default;
virtual void Initialize() = 0;
virtual void addWindow(QWidget* w, QObject* parent) = 0;
virtual QColor GetAccentColor() = 0;
virtual bool GetIsDarkTheme() = 0;
signals:
void AccentColorChanged(QColor);
void DarkLightThemeChanged(bool isDark);
};
QT_BEGIN_NAMESPACE
Q_DECLARE_INTERFACE(SpkDtkPlugin, "org.spark-store.client.dtkplugin")
QT_END_NAMESPACE

View File

@@ -0,0 +1,19 @@
#pragma once
#include <QWidget>
#include <QtPlugin>
#include "spkdtkplugin.h"
class SpkDtkPluginImpl : public SpkDtkPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.spark-store.client.dtkplugin")
Q_INTERFACES(SpkDtkPlugin)
public:
void addWindow(QWidget* w, QObject* parent) override;
void Initialize() override;
QColor GetAccentColor() override;
bool GetIsDarkTheme() override;
};

28
resource/default_config Normal file
View File

@@ -0,0 +1,28 @@
[url]
api=https://store.deepinos.org/api/
res=http://img.store.deepinos.org.cn/
repo=https://d.store.deepinos.org.cn/store/server.list
[dirs]
cache="*/.cache/spark-store/res/"
download="*/Downloads/spark-store/"
[download]
servers="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/"
[resource]
concurrent=5
[misc]
privacy_warned=0
distro=none
[ui]
theme=3
[pkgmgr]
apt_repo=""
[internal]
qss_path=":/stylesheets/stylesheets/default.css"

60
resource/icons/back.svg Normal file
View 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, custom)"
sodipodi:docname="back.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
style="fill:none;fill-rule:evenodd;stroke:#393939;stroke-width:1.38924;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:45.3543;stroke-opacity:1;paint-order:markers stroke fill;fill-opacity:1"
d="M 9.1126934,1.5875 3.8488777,6.3499999 9.1126934,11.1125"
id="path945" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,133 @@
<?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 (c4e8f9ed74, 2021-05-24)"
sodipodi:docname="broken-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
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"
units="px"
width="48px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="6.146021"
inkscape:cx="15.945276"
inkscape:cy="36.690405"
inkscape:window-width="1372"
inkscape:window-height="1049"
inkscape:window-x="68"
inkscape:window-y="2"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<sodipodi:guide
position="-1.0906364,2.8181404"
orientation="1,0"
id="guide9660" />
<sodipodi:guide
position="7.1936015,1.7762211"
orientation="1,0"
id="guide10203" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient10395">
<stop
style="stop-color:#808080;stop-opacity:0"
offset="0"
id="stop10391" />
<stop
style="stop-color:#000000;stop-opacity:0.48333037"
offset="0.84550369"
id="stop10751" />
<stop
style="stop-color:#4d4d4d;stop-opacity:0"
offset="1"
id="stop10393" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient10395"
id="radialGradient10557"
cx="7.1936016"
cy="8.282548"
fx="7.1936016"
fy="8.282548"
r="4.1442288"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g8764"
transform="rotate(-12.030961,2.0237324,6.2819556)">
<path
id="rect846"
style="opacity:1;fill:#a9a9a9;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 19.958984,10.617188 H 6.0507812 c -2.287583,0 -4.1308593,1.841323 -4.1308593,4.128906 v 19.060547 c 0,2.287583 1.8432763,4.128906 4.1308593,4.128906 H 21.111328 l -1.011719,-6.302735 3.404297,-3.363281 -4.607422,-3.847656 2.144532,-4.552734 -3.744141,-1.080079 z"
transform="scale(0.26458333)" />
<path
id="rect846-6"
style="opacity:1;fill:#3f3f3f;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 20.921875,26.113281 c -0.126766,0.138918 -0.240053,0.257037 -0.369141,0.400391 -2.09418,2.325637 -3.707031,-0.121094 -3.707031,-0.121094 0,0 -3.752996,-4.564976 -5.591797,-7.1875 -0.574624,-0.819539 -1.155085,-1.116745 -1.6835935,-1.138672 -1.1627186,-0.04824 -2.0683594,1.238282 -2.0683594,1.238282 l -3.9453125,5.326171 v 8.017579 c 0,2.009595 1.6173565,3.628906 3.6269532,3.628906 H 20.845703 l -0.746094,-4.644532 3.404297,-3.363281 z"
transform="scale(0.26458333)" />
</g>
<g
id="g8703"
transform="rotate(11.093101,12.853224,12.419804)">
<path
id="path8695"
style="opacity:1;fill:#a8a8a8;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 19.958984,10.617188 -2.662109,8.171874 3.744141,1.080079 -2.144532,4.552734 4.607422,3.847656 -3.404297,3.363281 1.011719,6.302735 h 14.646484 c 2.287584,0 4.128907,-1.841323 4.128907,-4.128906 V 14.746094 c 0,-2.287583 -1.841323,-4.128906 -4.128907,-4.128906 z"
transform="scale(0.26458333)" />
<path
id="path8698"
style="opacity:1;fill:#3f3f3f;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 20.921875,26.113281 2.582031,2.15625 -3.404297,3.363281 0.746094,4.644532 H 34.625 c 0.01113,0 0.0221,-0.0019 0.0332,-0.002 -0.254035,-0.338958 -0.510975,-0.705799 -0.769531,-1.111329 -2.620264,-3.550743 -4.692162,-7.743904 -6.351563,-11.451171 -1.457287,-3.219752 -3.052734,-1.304688 -3.052734,-1.304688 0,0 -1.632741,1.590337 -3.5625,3.705078 z"
transform="scale(0.26458333)" />
</g>
<ellipse
style="opacity:1;fill:#9b0000;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient10557);stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
id="path8868"
cx="7.1496811"
cy="8.282548"
rx="3.5439999"
ry="3.5442288" />
<g
id="g10389"
transform="translate(8.2842379,-0.78676148)">
<path
id="path9559"
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
d="m -1.7035189,7.3114741 0.6128825,2.5703855 0.61339925,-2.5703855 H -1.0906364 Z"
sodipodi:nodetypes="ccccc" />
<circle
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#808080;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
id="path10045"
cx="-1.0906364"
cy="10.634181"
r="0.46236092" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Some files were not shown because too many files have changed in this diff Show More