KOReader 外掛開發
KOReader 的前端是純 Lua(LuaJIT),運行在一個小型 widget 工具集上。外掛是一個名為 <名稱>.koplugin/ 的目錄,內含 main.lua(程式碼)與 _meta.lua(顯示資訊)。沒有建置步驟,因此最快正確的方式是遵循以下經過驗證的慣例,並使用附帶的腳本進行驗證,而不是猜測 API。
官方文件:https://koreader.rocks/doc/(API 參考,從原始碼產生)以及原始碼位於 https://github.com/koreader/koreader(`plugins/` 中有約 40 個真實外掛;plugins/hello.koplugin 是最簡範例)。若對某個函式簽章不確定,請查閱該模組的文件頁面或直接閱讀原始碼——KOReader API 在不同版本間會有變動,而文件網站是權威來源。
工作流程
- 產生骨架(新外掛):
python3 <skill>/scripts/new_plugin.py <id> "<Full Name>" [--dir <where>]會從範本建立<id>.koplugin/,包含選單項目、Dispatcher 動作、設定持久化與生命週期鉤子。從此處開始編輯,而非從頭撰寫。 - 實作:使用本文件中的模式;只在必要時才開啟參考檔案:
references/plugin-anatomy.md— 檔案佈局、載入器規則、is_doc_only、初始化順序、選單sorting_hint值references/ui-widgets.md— InfoMessage、ConfirmBox、InputDialog、MultiInputDialog、Menu、ButtonDialog、SpinWidget、Notification,附有可運作的程式碼片段references/events-and-lifecycle.md—onXxx處理器、廣播、排程、Dispatcher、閱讀器與檔案管理員上下文references/settings-and-storage.md— LuaSettings、G_reader_settings、每本書的文件設定、DataStorage 路徑references/testing-and-verification.md— 模擬器(./kodev run)、luacheck、busted 規格、裝置端安裝與日誌讀取references/pitfalls.md— 導致 KOReader 崩潰或無聲失效的錯誤;完成前請瀏覽一遍
- 驗證(務必在聲明可運作前執行):
python3 <skill>/scripts/validate_plugin.py <path/to/x.koplugin>。它會檢查結構、_meta.lua、選單接線、gettext 使用情況、LuaJIT 語法(luajit -bl)以及luacheck(若這些工具存在)。修正每個 ERROR;閱讀每個 WARN。然後誠實報告哪些已驗證、哪些未驗證——驗證器無法證明執行階段行為;只有模擬器或實際裝置可以(請參閱測試參考)。
最小可行外掛
-- _meta.lua
local _ = require("gettext")
return {
fullname = _("My Plugin"),
description = _([[在 Plugin management 中顯示的一行說明。]]),
}
-- main.lua
local Dispatcher = require("dispatcher")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
local MyPlugin = WidgetContainer:extend{
name = "myplugin", -- 唯一 ID,與目錄前綴相符
is_doc_only = false, -- true = 僅在書籍開啟時載入
}
function MyPlugin:onDispatcherRegisterActions()
Dispatcher:registerAction("myplugin_hello", {
category = "none", event = "MyPluginHello",
title = _("My Plugin: hello"), general = true,
})
end
function MyPlugin:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
end
function MyPlugin:addToMainMenu(menu_items)
menu_items.myplugin = {
text = _("My Plugin"),
sorting_hint = "more_tools",
callback = function() self:onMyPluginHello() end,
}
end
function MyPlugin:onMyPluginHello()
UIManager:show(InfoMessage:new{ text = _("Hello from my plugin") })
return true -- 事件已消耗
end
return MyPlugin
重要的規則(及其原因)
- 將每個使用者可見的字串包裹在
_()中 — KOReader 透過 gettext 進行翻譯;未包裹的字串永遠無法本地化。使用T = require("ffi/util").template進行插值(T(_("Page %1"), n)),而非..串聯,因為不同語言的詞序不同。 - 事件是名為
on<EventName>的方法;僅在消耗事件時返回true,否則其他 widget(包括 KOReader 核心)將無法收到它。 - 絕不阻塞 UI 執行緒。 網路呼叫、大型檔案掃描與
os.execute會凍結單執行緒的電子墨水裝置。對於長時間工作,請使用NetworkMgr:runWhenOnline、UIManager:scheduleIn、Trapper:wrap(子程序 + 進度)。 - 使用
LuaSettings進行持久化,並刻意呼叫:flush()。 設定檔位於DataStorage:getSettingsDir()中;在變更後(或在onFlushSettings中)呼叫:flush()— 崩潰或電量耗盡否則會遺失它們。 - 清理計時器(
UIManager:unschedule(fn))以及你在onCloseWidget/onCloseDocument中開啟的 widget;洩漏的排程回呼會持續喚醒裝置並耗電。 - 電子墨水感知 UI:偏好使用帶有
timeout的InfoMessage,避免動畫,保持最小程度的重新整理(僅在變更的 widget 上呼叫UIManager:setDirty(widget, "ui"))。 - 使用
logger(require("logger");logger.dbg/info/warn/err)而非print;輸出會傳送到模擬器中的 stdout 以及裝置上的crash.log。 - 保護可選依賴(
self.ui.statistics、self.ui.document、Device:hasFrontlight()):同一個外掛可能在檔案管理員或缺少該功能的裝置上載入。
回報結果
說明你建立/更改了什麼、確切的驗證指令及其結果,以及哪些部分尚未驗證(例如「未在模擬器中執行」)。如果驗證器或 luacheck 不可用,請如實說明,而非暗示通過檢查。