iRightMenu Pro 插件开发指南
本指南涵盖了开发 iRightMenu Pro 插件所需的全部知识。
目录
插件结构
每个插件至少包含两个文件:
- plugin.json - 声明元数据、菜单、设置(纯配置,无代码)
- main.lua - 实现菜单处理函数(业务逻辑)
拆成多个文件
逻辑变复杂时可以拆开——引擎为插件目录配好了 require 搜索路径:
com.example.my-plugin/
├── plugin.json
├── main.lua
├── parser.lua -- require("parser")
└── utils/
└── init.lua -- require("utils")
local parser = require("parser")
local utils = require("utils")
搜索路径就是插件目录,等价于:
package.path = <插件目录>/?.lua;<插件目录>/?/init.lua
只能加载 Lua 模块。
package.cpath被清空,C 扩展模块加载不了——插件运行在 沙箱里,这是刻意的限制。拆分后每个文件都要过检查:子模块里调用
app.shell.execute同样需要在plugin.json里声明shell权限,而主文件里看不出来。用tools/pluginlint可以自动查(它会跟着require递归扫描)。
配置文件 (plugin.json)
完整格式
{
"manifestVersion": 1,
"id": "com.example.my-plugin",
"name": "我的插件",
"version": "1.0.0",
"minEngineVersion": "2.0.0",
"author": "作者名",
"description": "插件描述",
"main": "main.lua",
"icon": "icon.png",
"locale": "zh-Hans",
"category": "file",
"type": "context",
"permissions": ["file", "http"],
"settings": {
"title": "插件设置",
"fields": [...]
},
"menus": [...]
}
基础字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
manifestVersion |
Int | 否 | plugin.json 格式规范版本(当前为 1) |
id |
String | 是 | 唯一标识符(反向域名格式) |
name |
String | 是 | 显示名称(支持 i18n 占位符) |
version |
String | 是 | 语义化版本(如 “1.0.0”) |
minEngineVersion |
String | 否 | 最低 iRightMenu 版本要求 |
author |
String | 否 | 作者名称 |
description |
String | 否 | 插件描述 |
main |
String | 否 | 主脚本文件名(默认 “main.lua”) |
icon |
String | 否 | 图标文件名(相对路径) |
locale |
String | 否 | 默认语言(如 “en”、”zh-Hans”) |
homepage |
String | 否 | 插件主页 URL(官网、源码仓库等) |
category |
String | 否 | 插件分类,用于商店展示。可选值:file、image、development、system、other(默认) |
type |
String | 否 | 插件类型:"context"(右键菜单,默认)或 "resident"(常驻插件) |
permissions |
[String] | 否 | 权限声明(省略=全部允许,[]=仅安全 API,["*"]=全部) |
权限系统
插件通过 permissions 字段声明所需 API 权限。权限分为 4 个等级:
L0 安全(Safe)— 无需声明,始终可用
| 权限名 | API 模块 | 说明 |
|---|---|---|
log |
app.log |
日志记录 |
context |
app.context |
Finder 上下文 |
dialog |
app.dialog |
对话框和表单 |
progress |
app.progress |
进度对话框 |
path |
app.path |
路径工具 |
i18n |
app.i18n |
国际化 |
json |
app.json |
JSON 操作 |
string |
app.string |
字符串工具 |
date |
app.date |
日期时间 |
regex |
app.regex |
正则表达式 |
notification |
app.notification |
系统通知 |
settings |
app.settings |
插件设置 |
thread |
app.thread |
并发/协程 |
uti |
app.uti |
类型标识符 |
url |
app.url |
URL 解析构建 |
csv |
app.csv |
CSV 解析生成 |
xml |
app.xml |
XML 解析生成 |
color |
app.color |
颜色工具 |
L1 标准(Standard)— 需在 permissions 中声明
| 权限名 | API 模块 | 说明 |
|---|---|---|
file |
app.file |
文件读写 |
http |
app.http |
HTTP 请求 |
archive |
app.archive |
归档操作 |
plist |
app.plist |
Plist 读写 |
defaults |
app.defaults |
UserDefaults 偏好 |
image |
app.image |
图片处理 |
crypto |
app.crypto |
加密编码 |
clipboard |
app.clipboard |
剪贴板 |
finder |
app.finder |
Finder 操作 |
system |
app.system |
系统信息 |
keychain |
app.keychain |
安全凭证存储 |
xattr |
app.xattr |
扩展属性/标签 |
trash |
app.trash |
废纸篓 |
metadata |
app.metadata |
Spotlight 元数据 |
pdf |
app.pdf |
PDF 操作 |
network |
app.network |
网络信息 |
power |
app.power |
电源/电池 |
screen |
app.screen |
屏幕/外观 |
audio |
app.audio |
音频控制 |
ocr |
app.ocr |
文字识别 |
qrcode |
app.qrcode |
二维码 |
wakelock |
app.wakelock |
防休眠 |
watcher |
app.watcher |
文件监听 |
share |
app.share |
macOS 分享 |
shortcuts |
app.shortcuts |
快捷指令 |
timer |
app.timer |
定时器 |
websocket |
app.websocket |
WebSocket 客户端 |
chooser |
app.chooser |
快速选择器 |
dock |
app.dock |
Dock 角标/弹跳 |
L2 敏感(Sensitive)— 安装时显示权限警告
| 权限名 | API 模块 | 说明 |
|---|---|---|
shell |
app.shell |
Shell 命令执行 |
database |
app.database |
SQLite 数据库 |
process |
app.process |
进程管理 |
application |
app.application |
应用程序控制 |
webview |
app.webview |
WebView 窗口 |
statusbar |
app.statusbar |
状态栏图标 |
window |
app.window |
窗口控制(需辅助功能权限) |
location |
app.location |
地理位置(首次调用弹系统授权) |
bluetooth |
app.bluetooth |
蓝牙状态与设备(首次调用弹系统授权) |
hardware_id |
(函数级) | 机器序列号、硬件 UUID、磁盘序列号等可识别本机的标识 |
login_item |
(函数级) | 设置开机自启(仅 app.system.setLoginItem 需要) |
最后两条不绑定整个模块,只门控个别函数。
app.system本身是 L1, 但里面取硬件标识的几个函数要额外声明hardware_id,改开机启动项要login_item。 不声明就调,运行时会拿到permissionDenied。
L3 危险(Dangerous)— 安装时显示强烈安全警告
| 权限名 | API 模块 | 说明 |
|---|---|---|
applescript |
app.applescript |
AppleScript(完整系统访问) |
keyboard |
app.keyboard |
键盘模拟(需辅助功能权限) |
mouse |
app.mouse |
鼠标模拟(需辅助功能权限) |
hotkey |
app.hotkey |
全局热键(需辅助功能权限) |
权限声明示例
// 只使用安全 API(无需声明)
{"permissions": []}
// 需要文件和网络
{"permissions": ["file", "http"]}
// 需要全部权限
{"permissions": ["*"]}
// 省略 permissions 字段 = 允许全部(兼容旧插件)
{}
菜单配置
{
"menus": [
{
"id": "menu_action",
"title": "执行操作",
"description": "菜单项描述",
"icon": "action.png",
"enabled": true,
"showInStatusBar": false,
"primaryMenu": false,
"dynamic": false,
"showOnFiles": true,
"showOnFolders": false,
"fileMatchType": "none",
"fileMatchPatterns": [],
"fileExecutableOnly": false,
"minFiles": 1,
"maxFiles": 0,
"folderMatchType": "none",
"folderMatchPatterns": [],
"minFolders": 0,
"maxFolders": 0,
"notifyType": "none",
"requireConfirm": false,
"playSound": false,
"soundPath": null
}
]
}
菜单字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id |
String | 必需 | 菜单项唯一标识,用于绑定点击回调和动态行为 |
title |
String | 必需 | 菜单项文本(支持 i18n) |
description |
String | ”” | 工具提示/描述 |
icon |
String | null | 菜单图标文件名 |
enabled |
Boolean | true | 是否启用 |
showInContextMenu |
Boolean | true | 在右键菜单中显示 |
showInStatusBar |
Boolean | false | 在状态栏菜单中显示 |
primaryMenu |
Boolean | false | 在主菜单中显示(非子菜单) |
dynamic |
Boolean | false | 启用动态标题(见「动态标题」,只在状态栏菜单生效) |
showInStatusBar让菜单项出现在 iRightMenu 自己的状态栏菜单里,无需额外权限。 若要创建插件自己独立的菜单栏图标(持续显示某个值),用app.statusbar—— 那是 L2 sensitive 权限。
文件匹配规则
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showOnFiles |
Boolean | true | 选中文件时显示 |
fileMatchType |
String | “none” | 文件匹配模式 |
fileMatchPatterns |
Array | [] | 扩展名/正则匹配模式 |
fileExecutableOnly |
Boolean | false | 仅匹配可执行文件 |
minFiles |
Number | 0 | 最少选中文件数(0=不限) |
maxFiles |
Number | 0 | 最多选中文件数(0=不限) |
文件夹匹配规则
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showOnFolders |
Boolean | true | 选中文件夹时显示 |
folderMatchType |
String | “none” | 文件夹匹配模式(仅支持 “none”、”regex”) |
folderMatchPatterns |
Array | [] | 正则匹配模式 |
minFolders |
Number | 0 | 最少选中文件夹数(0=不限) |
maxFolders |
Number | 0 | 最多选中文件夹数(0=不限) |
执行配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
notifyType |
String | “none” | 执行后通知方式:"none"(不通知)、"notification"(系统通知)。插件默认 none,插件自行通过 app.notification API 管理反馈 |
requireConfirm |
Boolean | false | 执行前是否弹出确认对话框 |
playSound |
Boolean | false | 执行后是否播放提示音 |
soundPath |
String | null | 提示音文件名(如 "Glass.aiff"),为空时使用系统默认提示音 |
文件匹配类型
| 类型 | 说明 | fileMatchPatterns |
|---|---|---|
none |
通用匹配,不限制文件类型 | 忽略 |
extension |
按文件扩展名匹配 | ["jpg", "png", "gif"] |
regex |
按正则表达式匹配文件名 | ["^IMG_\\d+"] |
executable |
仅匹配可执行文件 | 忽略 |
脚本文件 (main.lua)
基本结构
-- 继承 Plugin 基类创建插件类
local MyPlugin = Plugin:extend()
-- 初始化:注册处理函数
function MyPlugin:init()
-- 注册菜单动作处理函数
self:registerHandler("menu_id", self.handleAction)
app.log.info("插件已初始化")
end
-- 菜单动作处理函数
-- @param context {selectedFiles, currentDirectory}
function MyPlugin:handleAction(context)
local files = context.selectedFiles
local dir = context.currentDirectory
-- 你的业务逻辑...
end
-- 动态标题(可选,只在状态栏菜单生效——见「动态标题」一节)
-- @return string 新标题;nil 表示用 plugin.json 里的静态标题
function MyPlugin:getMenuTitle(menuId, context)
return nil
end
-- 必须返回插件类(引擎自动实例化)
return MyPlugin
context 上下文对象
传递给处理函数的 context 对象包含:
context = {
selectedFiles = {"/path/to/file1", "/path/to/file2", ...},
currentDirectory = "/path/to/current/dir"
}
Plugin 基类
Plugin 基类已预加载到全局环境中。
创建插件
-- 继承基类
local MyPlugin = Plugin:extend()
-- 实现 init()
function MyPlugin:init()
-- 在这里注册处理函数
end
-- 返回插件类(必需,引擎自动实例化)
return MyPlugin
元数据方法
| 方法 | 返回值 | 说明 |
|---|---|---|
self:getId() |
String | 插件 ID |
self:getName() |
String | 插件名称 |
self:getVersion() |
String | 插件版本 |
self:getAuthor() |
String | 插件作者 |
self:getDescription() |
String | 插件描述 |
self:getMetadata() |
Table | 完整元数据 |
self:getInstallPath() |
String | 插件安装目录路径 |
处理函数注册
-- 注册菜单动作处理函数
self:registerHandler("menu_id", self.handleAction)
菜单项显隐用 plugin.json 的声明式字段控制(showOnFiles / fileMatchPatterns /
minFiles 等),不需要注册回调。
重要:使用方法引用(
self.methodName)而非闭包。 处理函数接收(self, context)两个参数,使用闭包会导致参数错位。
生命周期方法(可选)
function MyPlugin:onLoad()
-- 插件环境加载完成后调用(每次启动)
end
function MyPlugin:onUnload()
-- 插件环境即将卸载时调用
end
function MyPlugin:onInstall()
-- 首次安装后调用(在 onLoad 之后触发)
end
function MyPlugin:onUpdate(oldVersion)
-- 插件更新后调用(在 onLoad 之后触发)
-- oldVersion: 更新前的版本号
app.log.info("Updated from " .. oldVersion)
end
function MyPlugin:onUninstall()
-- 卸载前调用(在 onUnload 之前触发,可用于清理资源)
end
调用时序:
| 场景 | 调用顺序 |
|---|---|
| 应用启动 | onLoad |
| 安装插件 | onLoad → onInstall |
| 更新插件 | 旧插件 onUnload → 新插件 onLoad → onUpdate(oldVersion) |
| 卸载插件 | onUninstall → onUnload |
| 禁用/启用 | onUnload / onLoad |
注意:
onInstall和onUpdate在插件环境完全就绪后才触发,可安全使用所有 API。onUninstall在文件删除前同步调用,适合清理 Keychain 凭证、临时文件等资源。
访问插件资源
function MyPlugin:init()
-- 获取内置工具路径
local tool = self:getInstallPath() .. "/bin/mytool"
-- 使用 app.path.join 拼接路径
local config = app.path.join(
self:getInstallPath(),
"config",
"default.json"
)
-- 检查资源是否存在
if app.path.exists(self:getInstallPath() .. "/resources/data.json") then
app.log.info("资源文件存在")
end
end
菜单匹配规则
基本匹配
{
"menus": [{
"id": "process",
"title": "处理",
"showOnFiles": true,
"showOnFolders": false,
"minFiles": 1
}]
}
此菜单在以下情况显示:
- 至少选中 1 个文件
- 未选中文件夹
扩展名匹配
{
"menus": [{
"id": "resize_image",
"title": "调整图片大小",
"fileMatchType": "extension",
"fileMatchPatterns": ["jpg", "jpeg", "png", "gif", "webp"],
"showOnFiles": true,
"showOnFolders": false
}]
}
此菜单仅对图片文件显示。
正则匹配
{
"menus": [{
"id": "process_screenshots",
"title": "处理截图",
"fileMatchType": "regex",
"fileMatchPatterns": ["^Screenshot.*\\.png$", "^截屏.*\\.png$"],
"showOnFiles": true
}]
}
动态标题
在 plugin.json 里声明 "dynamic": true,菜单就能每次弹出时重新生成标题——实现
getMenuTitle(menuId, context) 即可(约定方法名,不需要注册)。返回 nil 表示这一项
用 plugin.json 里的静态标题。
function MyPlugin:getMenuTitle(menuId, context)
if menuId ~= "toggle_proxy" then
return nil -- 其余菜单项保持静态标题
end
if self:isProxyOn() then
return app.i18n.t("menu.turn_off")
end
return app.i18n.t("menu.turn_on")
end
用它把信息直接显示在菜单上,用户不点也能看见——这往往比点开一个对话框更有用:
function NetworkIP:getMenuTitle(menuId, context)
if menuId ~= "copy_local_ip" then return nil end
local ip = app.network.localIP()
if not ip then
return app.i18n.t("menu.offline")
end
-- 菜单上直接显示「本机 IP:192.168.1.100」
return app.i18n.t("menu.copy_local_ip_with_value", { ip = ip })
end
注意:
getMenuTitle在每次菜单弹出时都会被调用,务必保持轻量。 不要在里面做网络请求或读大文件——那会让右键菜单卡住。上面第二个例子里app.network.localIP()读的是本地网络接口,是安全的;换成app.network.externalIP()(要访问外部服务)就不行。⚠️ 生效范围:只有状态栏菜单。
Finder 右键菜单由 FinderSync 扩展渲染,那是一个独立进程、里面没有 Lua 引擎 (它只负责画菜单,点击后把动作转交给主 App),所以那边只能显示
plugin.json里的 静态标题。用户可开启的「覆盖系统右键」虽然由主 App 渲染、技术上跑得了 Lua, 但同样只显示静态标题——同一个手势不该因为一个为别的目的存在的开关就给出不同的 菜单。因此:
- 静态
title必须能独立成立——「切换代理」比「关闭代理」好,后者在右键菜单里 可能和实际状态相反- 别把关键信息只放在动态标题里。像「本机 IP:192.168.1.5」这种,右键菜单里 IP 值就没了,这类插件应该声明
showInContextMenu: false,只在状态栏出现
pluginlint会拦这条:声明了dynamic却没开showInStatusBar,等于永不生效,直接报错。
用代码控制菜单项显隐的 registerVisibility 已移除——它受同样的限制,但在 Finder
右键菜单里会被完全忽略、插件想隐藏也隐藏不掉,作者收不到任何反馈。请改用 plugin.json
的声明式字段(showOnFiles / fileMatchPatterns / minFiles 等),那些是数据库字段,
所有菜单面都生效。旧插件调用它不报错,只在日志里留一条警告。
插件设置
定义设置
{
"settings": {
"title": "我的插件设置",
"fields": [
{
"key": "api_key",
"type": "text",
"label": "API 密钥",
"default": "",
"placeholder": "请输入你的 API 密钥"
},
{
"key": "quality",
"type": "number",
"label": "质量",
"default": 80
},
{
"key": "auto_process",
"type": "checkbox",
"label": "自动处理",
"default": true
},
{
"key": "output_format",
"type": "select",
"label": "输出格式",
"default": "png",
"options": ["png", "jpg", "webp"]
},
{
"key": "output_dir",
"type": "folder",
"label": "输出目录",
"default": ""
}
]
}
}
字段类型
| 类型 | 说明 | 返回值类型 |
|---|---|---|
text |
单行文本输入 | String |
number |
数字输入 | Number |
checkbox |
复选框 | Boolean |
select |
下拉菜单 | String |
folder |
文件夹选择器 | String(路径) |
file |
文件选择器 | String(路径) |
在 Lua 中使用设置
-- 获取设置值(带默认值)
local apiKey = app.settings.get("api_key", "")
local quality = app.settings.get("quality", 80)
-- 设置值
app.settings.set("last_used", os.time())
-- 获取所有设置
local all = app.settings.getAll()
-- 删除设置
app.settings.remove("temporary_value")
设置存在 <App Group 容器>/PluginData/<插件 id>/settings.json,不在插件目录里:
升级插件时插件目录会被整份替换,放在那儿的设置每次升级都会丢。
插件自己的可写目录
要落盘的其它东西——缓存、app.database 的库文件、下载的资源、运行状态——写到
app.context.dataDirectory():
local dir = app.context.dataDirectory()
app.file.write(dir .. "/cache.json", app.json.stringify(data))
local db = app.database.open(dir .. "/history.db")
跟 app.settings 同一个目录,升级插件不受影响,卸载插件时一并清除。
不要写 app.context.pluginDirectory()。 那是插件的安装目录,属于随包内容:
升级时整个被替换,写进去的会丢;改动随包分发的文件还会让签名校验失败,用户那边
表现为插件加载不出来。它只用来读随包的资源。
国际化
目录结构
com.example.my-plugin/
├── plugin.json
├── main.lua
└── locales/
├── en.json
└── zh-Hans.json
在 plugin.json 中使用占位符
{
"name": "{{plugin.name}}",
"description": "{{plugin.description}}",
"menus": [
{
"id": "action",
"title": "{{menu.action}}"
}
],
"settings": {
"title": "{{settings.title}}",
"fields": [
{
"key": "option",
"type": "text",
"label": "{{settings.option}}"
}
]
}
}
语言文件
locales/en.json(平铺 dot-notation 格式)
{
"plugin.name": "My Plugin",
"plugin.description": "A useful plugin",
"menu.action": "Do Something",
"settings.title": "Settings",
"settings.option": "Option Label",
"messages.success": "Processed %d files successfully",
"messages.error": "An error occurred: %s"
}
locales/zh-Hans.json
{
"plugin.name": "我的插件",
"plugin.description": "一个实用的插件",
"menu.action": "执行操作",
"settings.title": "设置",
"settings.option": "选项标签",
"messages.success": "成功处理了 %d 个文件",
"messages.error": "发生错误:%s"
}
在 Lua 中使用
-- 获取翻译字符串
local title = app.i18n.t("plugin.name")
-- 带格式参数
local msg = app.i18n.t("messages.success", 5)
-- 英文:"Processed 5 files successfully"
-- 中文:"成功处理了 5 个文件"
-- 带默认值
local text = app.i18n.t("unknown.key", nil, "默认文本")
-- 检查 key 是否存在
if app.i18n.has("messages.custom") then
-- ...
end
-- 当前语言
local locale = app.i18n.locale() -- "en" 或 "zh-Hans"
-- 可用语言列表
local locales = app.i18n.locales() -- {"en", "zh-Hans"}
调试技巧
日志
-- 日志级别(从低到高)
app.log.verbose("详细信息") -- 仅插件日志
app.log.debug("调试信息") -- 仅插件日志
app.log.info("普通信息") -- 仅插件日志
app.log.warning("警告") -- 插件 + 系统日志
app.log.error("错误") -- 插件 + 系统日志
-- 获取日志文件路径
local logPath = app.log.getPath()
-- 读取最后 N 行
local content = app.log.read(100)
-- 清空日志文件
app.log.clear()
日志位置
- 插件日志:
~/Library/Group Containers/group.net.ymlab.iRightMenu.pro/Logs/Plugins/{plugin_id}.log - 系统日志:
~/Library/Group Containers/group.net.ymlab.iRightMenu.pro/Logs/iRightMenu.log
实时查看日志
# 查看所有日志
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/*.log
# 查看特定插件
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/Plugins/com.example.myplugin.log
# 过滤错误
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/*.log | grep -i error
常见问题
| 问题 | 解决方案 |
|---|---|
| 插件不显示 | 检查 plugin.json JSON 语法 |
| 菜单不出现 | 检查匹配规则(showOnFiles、fileMatchType 等) |
| 点击无响应 | 确认 registerHandler 的 ID 与菜单 ID 匹配 |
| API 返回 nil | 查看日志获取详细错误信息 |
| 设置未保存 | 确保使用正确的 key 名称 |
调试模式
开发时启用详细日志:
function MyPlugin:init()
app.log.verbose("=== 插件初始化开始 ===")
app.log.verbose("安装路径: " .. self:getInstallPath())
app.log.verbose("版本: " .. self:getVersion())
-- ... 注册处理函数 ...
app.log.verbose("=== 插件初始化完成 ===")
end
最佳实践
1. 使用唯一的插件 ID
com.yourcompany.plugin-name
2. 优雅地处理错误
function MyPlugin:processFiles(context)
local success, err = pcall(function()
for _, file in ipairs(context.selectedFiles) do
self:processFile(file)
end
end)
if not success then
app.log.error("处理失败: " .. tostring(err))
app.dialog.alert({title = "错误", message = "文件处理失败"})
end
end
3. 提供用户反馈
function MyPlugin:longOperation(context)
-- 长时间操作显示进度
local p = app.progress.create({
title = "处理中",
message = "准备中...",
indeterminate = false
})
local files = context.selectedFiles
for i, file in ipairs(files) do
p:update(
(i / #files) * 100,
"正在处理: " .. app.path.basename(file)
)
self:processFile(file)
end
p:close()
-- 完成时发送通知
app.notification.show("完成", "已处理 " .. #files .. " 个文件")
end
4. 始终提供默认值
local quality = app.settings.get("quality", 80) -- 默认值:80
local format = app.settings.get("format", "png") -- 默认值:"png"
5. 记录重要操作
function MyPlugin:deleteFiles(context)
app.log.info("[MyPlugin] 开始删除操作")
app.log.info("[MyPlugin] 待删除文件数: " .. #context.selectedFiles)
for _, file in ipairs(context.selectedFiles) do
app.log.info("[MyPlugin] 删除: " .. file)
-- ...
end
app.log.info("[MyPlugin] 删除操作完成")
end
6. 支持国际化
即使最初只支持一种语言,也使用 i18n 占位符,方便将来添加翻译。
7. 验证用户输入
function MyPlugin:renameFiles(context)
local __form = app.dialog.form({
title = "输入新名称",
fields = {{type = "text", id = "value", label = "输入", default = ""}}
})
local newName = __form and __form.value
if not newName or newName == "" then
return -- 用户取消或输入为空
end
-- 验证文件名
if newName:match('[/\\:*?"<>|]') then
app.dialog.alert({title = "无效名称", message = "文件名包含非法字符"})
return
end
-- 继续重命名...
end
8. 清理资源
function MyPlugin:onUnload()
-- 关闭数据库连接
if self.db then
self.db:close()
self.db = nil
end
-- 取消待处理的操作
thread.stopAll()
end