ONEMORE的诞生
在公司其他事业部有一个工具包bsptools集成了一些常用的工具,咨询了作者不开源。
同时设计的UI界面因为偏重于实用,界面设计也较为复古
受到此软件的启发,有了制作一款高度自定义的集成工具的想法,OneMore由此诞生。
下图为软件登录界面
框架设计
OneMore使用了pyqt6+fluent-widgets进行开发,以侧边导航栏为分类,以新建标签页的形式来呈现每个工具的功能。将工具所需要的变量以可视化的方法交由使用人员配置进行。
app/common: 存放通用的一些接口
app/resource: 存放资源文件,包括图标,背景图片,语言翻译文件等等
app/main_window.py: 主窗口文件
app/mtk_interface.py: mtk界面设计界面
app/qcom_interface.py: 高通界面设计界面
app/register_window.py: 账号登录界面
app/setting_interface.py: 设置界面
tools: 存放一些通用的工具
plugins: 插件安装目录
release: 存放插件的压缩包目录
已实现功能
完成登录界面
设置界面设计完成,包括深浅色模式切换、语言选择等等
导航栏设计完成,方便后续新增分类
标签页设计完成,方便后续分类中的插件新增标签页的
插件执行页面设计完成,以工具简介/工具详细描述/工具执行参数可视化等元素
上架插件市场
软件在线更新插件功能
插件开发指南
插件组成元素
一个插件包含5个部分
plugin.py
from loguru import logger
from app.common.utils import generate_uuid
from plugins.Linux_Ramdump_Parser.LinuxRamdumpParserinterface import LinuxRamdumpParserInterface as LRDP # 按照实际插件信息更改
from plugins.Linux_Ramdump_Parser.LinuxRamdumpParserinterface import LinuxRamdumpParserCardsInfo # 按照实际插件信息更改
import os
UNIQUE_NAME = "Linux Ramdump Parser" # 更改插件的名字
CURRENT_PLUGIN_DIR = os.path.dirname(__file__)
def get_route_key():
"""
Generate a unique route key for the plugin.
"""
return f"{UNIQUE_NAME} {generate_uuid()}"
def register(main_window):
def on_open():
routekey = get_route_key()
interface = LRDP(mainWindow=main_window) # 按照实际插件信息更改
interface.addTab(routeKey=routekey, text=routekey, icon='logo.png')
# 将插件的路由键添加到 main_window 的 TabRouteKeys 中, 以便於处理 Tab 切换
main_window.TabRouteKeys.append(routekey)
def on_tab_changed(route_key: str):
state = -1
for TabRouteKey in main_window.TabRouteKeys:
if TabRouteKey == route_key:
logger.info("[TAB CHANGED] Tab change to {}".format(TabRouteKey))
widget = main_window.showInterface.findChild(LinuxRamdumpParserCardsInfo, TabRouteKey) # 按照实际插件信息更改
main_window.showInterface.setCurrentWidget(widget)
state = 1
break
if state != 1:
logger.warning(f"[TAB WARNING] can not handle Tab change,can not find route_key={route_key}")
appcard = main_window.qcomInterface.addCard(os.path.join(CURRENT_PLUGIN_DIR, "logo.png"), "Linux Ramdump Parser", '@designed by iliuqi.', "Linux Ramdump Parser") # 按照实际插件信息更改
main_window.registerPluginOpener(UNIQUE_NAME, on_open)
main_window.registerTabChangedHandler(UNIQUE_NAME, on_tab_changed)
上述注释标注的地方,需要按照插件的实际信息更改,其余地方如果不了解,请不要更改!
备注:
插件目前共有三个分类,分别是通用、高通和MTK。
如果需要控制插件注册到特定的分类中,则需要更改上述demo的倒数第三行addCard
比如控制插件注册到通用分类中,则该行改为
appcard = main_window.generalInterface.addCard(os.path.join(CURRENT_PLUGIN_DIR, "logo.png"), "Linux Ramdump Parser", '@designed by iliuqi.', "Linux Ramdump Parser") # 按照实际插件信息更改
metadata.json
{
"name": "DTB2DTS",
"description": "将设备树二进制文件(DTB)转换为设备树源文件(DTS)的工具",
"version": "1.0.0",
"author": "iliuqi",
"logo": "logo.png",
"entry": "plugin.py",
"zip_url": "https://raw.githubusercontent.com/miniLQ/onemore/refs/heads/dev/release/DTB2DTS.zip"
}
按照实际插件情况,设置插件的metadata.json
备注:目前,插件metadata.json中的zip_url属性无用,只是预留!插件市场下载更新插件是根据源码中的plugins/plugin_index.json中定义的插件下载地址进行处理的
插件interface
interface是描述插件的标签卡,也就是下图显示的内容
关于这个interface的py脚本的详细逻辑,不在此详细描述,只描述需要值得注意的地方
AppInfoCard
AppInfoCard也就是上图中最上面的那一个块。
class AppInfoCard(SimpleCardWidget):
""" App information card """
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.iconLabel = ImageLabel(os.path.join(CURRENT_PLUGIN_DIR, "logo.png"), self) # 设置logo
self.iconLabel.setBorderRadius(8, 8, 8, 8)
self.iconLabel.scaledToWidth(120)
self.nameLabel = TitleLabel('Android Images Editor', self) # 设置标题
#self.companyLabel = HyperlinkLabel(
# QUrl('https://qfluentwidgets.com'), 'Shokokawaii Inc.', self)
self.companyLabel = CaptionLabel('@Designed by cfig.', self) # 更改作者信息
#self.installButton.setFixedWidth(160)
#self.scoreWidget = StatisticsWidget('平均', '5.0', self)
#self.separator = VerticalSeparator(self)
#self.commentWidget = StatisticsWidget('评论数', '3K', self)
self.descriptionLabel = BodyLabel(
'本工具为github上的开源项目,本人只是将此工具直接集成到onemore中\n作者:cfig\ngithub地址:https://github.com/cfig/Android_boot_image_editor', self) # 更改简要描述
self.descriptionLabel.setWordWrap(True)
self.tagButton = PillPushButton('android', self) # 设置tag
self.tagButton.setCheckable(False)
setFont(self.tagButton, 12)
self.tagButton.setFixedSize(80, 32)
self.tagButton2 = PillPushButton('unpack', self) # 设置tag
self.tagButton2.setCheckable(False)
setFont(self.tagButton2, 12)
self.tagButton2.setFixedSize(80, 32)
DescriptionCard
这个选项卡是详细描述部分
class DescriptionCard(HeaderCardWidget):
""" Description card """
def __init__(self, parent=None):
super().__init__(parent)
self.descriptionLabel = BodyLabel(
'本工具为github上的开源项目,本人只是将此工具直接集成到onemore中\n作者:cfig\ngithub地址:https://github.com/cfig/Android_boot_image_editor\n'
'使用本工具前必须先保证安装好JDK11+的java环境!!!', self)
self.descriptionLabel.setWordWrap(True)
self.viewLayout.addWidget(self.descriptionLabel)
self.setTitle('描述')
self.setBorderRadius(8)
SettingsCard
这里为自定义可视化参数的选项卡,具体细节参考已开发的插件
runButtonClicked
这个是点击运行按钮后,会跳转的函数,我们需要在这里面封装我们需要执行的操作
def runButtonClicked(self):
logger.info("Run Button Clicked")
logger.info("image file path: {}".format(self.inputfile))
logger.info("output directory: {}".format(self.output))
if self.comboBox.currentText() == "始终显示":
shell = True
else:
shell = False
logger.info("Display terminal: {}".format(shell))
if self.output == "" or self.inputfile == "":
self.showNoSelectFileFlyout()
# 如果vmlinuxfile的文件名不为vmlinux
elif not self.inputfile.endswith(".img"):
# 提示vmlinux文件名不为vmlinux
self.showFileStyleErrorFlyout()
else:
self.stateTooltip = StateToolTip('正在解析', '客官请耐心等待哦~~', self)
self.bottomStateLayout.addWidget(self.stateTooltip, 0, Qt.AlignmentFlag.AlignRight)
self.vBoxLayout.addLayout(self.bottomStateLayout)
# 显示状态提示
self.stateTooltip.show()
# runbuton按钮设置为不可点击
self.runButton.setDisabled(True)
command = "copy /Y {} {} && CD /d {} && {} unpack && xcopy /Y /E /I {} {} && {} clear".format(
self.inputfile,
ANDROID_BOOT_IMAGE_EDITOR_PATH,
ANDROID_BOOT_IMAGE_EDITOR_PATH,
"gradlew.bat",
os.path.normpath(os.path.join(ANDROID_BOOT_IMAGE_EDITOR_PATH, "build", "unzip_boot")).replace(os.sep, os.path.normcase(os.sep)),
os.path.normpath(os.path.join(self.output, "unzip_boot")).replace(os.sep, os.path.normcase(os.sep)),
"gradlew.bat")
self.start_task(command, shell=shell)
插件上传
开发好后的插件,需要压缩成zip格式压缩包,上传至master分支的release目录下
同时需要更新plugin_index.json上架市场
上架插件市场
[
{
"name": "DTB2DTS",
"description": "将设备树二进制文件(DTB)转换为设备树源文件(DTS)的工具",
"version": "1.0.0",
"author": "iliuqi",
"logo": "plugin_resources/logo/logo_DTB2DTS.png",
"entry": "plugin.py",
"zip_url": "https://raw.githubusercontent.com/miniLQ/onemore/refs/heads/master/release/DTB2DTS.zip"
},
{
"name": "Android_Images_Unpack",
"description": "高通平台开发提供给研发人员进行TZ log解析的一个工具",
"version": "v15_r1",
"author": "iliuqi",
"logo": "plugin_resources/logo/logo_Android_Images_Unpack.png",
"entry": "plugin.py",
"zip_url": "https://raw.githubusercontent.com/miniLQ/onemore/refs/heads/master/release/Android_Images_Unpack.zip"
},
...
]
插入的内容与metadata.json内容保持一致即可
共同开发
本着Linux开源的思想,本项目完全开源!
项目地址:
https://github.com/miniLQ/onemore
如果您对此项目感兴趣,并且有能力协助一起开发,期待您的PR(https://github.com/miniLQ/onemore/pulls)。
如果你对此项目有一些idea,也可以在issue(https://github.com/miniLQ/onemore/issues)提出您的建议,谢谢!