mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
* implements a Provider singleton, to be used by thirdparty plugins * exporter: support for thirdparty providers * splits plugin loading into two steps: discovery and load 1. get a list of all candidate plugins to load for the different paths 2. sort providers before on the rest of them and try to load them
301 lines
9.9 KiB
Lua
301 lines
9.9 KiB
Lua
local lfs = require("libs/libkoreader-lfs")
|
|
local logger = require("logger")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
|
|
local DEFAULT_PLUGIN_PATH = "plugins"
|
|
|
|
-- plugin names that were removed and are no longer available.
|
|
local OBSOLETE_PLUGINS = {
|
|
autofrontlight = true,
|
|
backgroundrunner = true,
|
|
calibrecompanion = true,
|
|
evernote = true,
|
|
goodreads = true,
|
|
kobolight = true,
|
|
send2ebook = true,
|
|
storagestat = true,
|
|
zsync = true,
|
|
}
|
|
|
|
local DEPRECATION_MESSAGES = {
|
|
remove = _("This plugin is unmaintained and will be removed soon."),
|
|
feature = _("The following features are unmaintained and will be removed soon:"),
|
|
}
|
|
|
|
local function isProvider(name)
|
|
return name:sub(1, 8) == "provider"
|
|
end
|
|
|
|
local function sortProvidersFirst(v1, v2)
|
|
if isProvider(v1.name) and isProvider(v2.name) then
|
|
return v1.path < v2.path
|
|
elseif isProvider(v1.name) then
|
|
return true
|
|
elseif isProvider(v2.name) then
|
|
return false
|
|
else
|
|
return v1.path < v2.path
|
|
end
|
|
end
|
|
|
|
local function deprecationFmt(field)
|
|
local s
|
|
if type(field) == "table" then
|
|
local f1, f2 = DEPRECATION_MESSAGES[field[1]], field[2]
|
|
if not f2 then
|
|
s = string.format("%s", f1)
|
|
else
|
|
s = string.format("%s: %s", f1, f2)
|
|
end
|
|
end
|
|
if not s then
|
|
return nil, ""
|
|
end
|
|
return true, s
|
|
end
|
|
|
|
-- Deprecated plugins are still available, but show a hint about deprecation.
|
|
local function getMenuTable(plugin)
|
|
local t = {}
|
|
t.name = plugin.name
|
|
t.fullname = string.format("%s%s", plugin.fullname or plugin.name,
|
|
plugin.deprecated and " (" .. _("outdated") .. ")" or "")
|
|
|
|
local deprecated, message = deprecationFmt(plugin.deprecated)
|
|
t.description = string.format("%s%s", plugin.description,
|
|
deprecated and "\n\n" .. message or "")
|
|
return t
|
|
end
|
|
|
|
local function sandboxPluginEventHandlers(plugin)
|
|
for key, value in pairs(plugin) do
|
|
if key:sub(1, 2) == "on" and type(value) == "function" then
|
|
plugin[key] = function(self, ...)
|
|
local ok, re = pcall(value, self, ...)
|
|
if ok then
|
|
return re
|
|
else
|
|
logger.err("failed to call event handler", key, re)
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local PluginLoader = {
|
|
show_info = true,
|
|
enabled_plugins = nil,
|
|
disabled_plugins = nil,
|
|
loaded_plugins = nil,
|
|
all_plugins = nil,
|
|
}
|
|
|
|
function PluginLoader:_discover()
|
|
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled")
|
|
if type(plugins_disabled) ~= "table" then
|
|
plugins_disabled = {}
|
|
end
|
|
-- disable obsolete plugins
|
|
for element in pairs(OBSOLETE_PLUGINS) do
|
|
plugins_disabled[element] = true
|
|
end
|
|
|
|
local discovered = {}
|
|
local lookup_path_list = { DEFAULT_PLUGIN_PATH }
|
|
local extra_paths = G_reader_settings:readSetting("extra_plugin_paths")
|
|
if extra_paths then
|
|
if type(extra_paths) == "string" then
|
|
extra_paths = { extra_paths }
|
|
end
|
|
if type(extra_paths) == "table" then
|
|
for _,extra_path in ipairs(extra_paths) do
|
|
local extra_path_mode = lfs.attributes(extra_path, "mode")
|
|
if extra_path_mode == "directory" and extra_path ~= DEFAULT_PLUGIN_PATH then
|
|
table.insert(lookup_path_list, extra_path)
|
|
end
|
|
end
|
|
else
|
|
logger.err("extra_plugin_paths config only accepts string or table value")
|
|
end
|
|
else
|
|
local data_dir = require("datastorage"):getDataDir()
|
|
if data_dir ~= "." then
|
|
local extra_path = data_dir .. "/plugins/"
|
|
G_reader_settings:saveSetting("extra_plugin_paths", { extra_path })
|
|
table.insert(lookup_path_list, extra_path)
|
|
end
|
|
end
|
|
for _, lookup_path in ipairs(lookup_path_list) do
|
|
logger.info("Looking for plugins in directory:", lookup_path)
|
|
for entry in lfs.dir(lookup_path) do
|
|
local plugin_root = lookup_path.."/"..entry
|
|
local mode = lfs.attributes(plugin_root, "mode")
|
|
-- valid koreader plugin directory
|
|
if mode == "directory" and entry:find(".+%.koplugin$") then
|
|
local mainfile = plugin_root.."/main.lua"
|
|
local metafile = plugin_root.."/_meta.lua"
|
|
local disabled = false
|
|
if plugins_disabled and plugins_disabled[entry:sub(1, -10)] then
|
|
mainfile = metafile
|
|
disabled = true
|
|
end
|
|
local __, name = util.splitFilePathName(plugin_root)
|
|
|
|
table.insert(discovered, {
|
|
["main"] = mainfile,
|
|
["meta"] = metafile,
|
|
["path"] = plugin_root,
|
|
["disabled"] = disabled,
|
|
["name"] = name,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
return discovered
|
|
end
|
|
|
|
function PluginLoader:_load(t)
|
|
-- keep reference to old value so they can be restored later
|
|
local package_path = package.path
|
|
local package_cpath = package.cpath
|
|
|
|
local mainfile, metafile, plugin_root, disabled
|
|
for _, v in ipairs(t) do
|
|
mainfile = v.main
|
|
metafile = v.meta
|
|
plugin_root = v.path
|
|
disabled = v.disabled
|
|
package.path = string.format("%s/?.lua;%s", plugin_root, package_path)
|
|
package.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath)
|
|
local ok, plugin_module = pcall(dofile, mainfile)
|
|
if not ok or not plugin_module then
|
|
logger.warn("Error when loading", mainfile, plugin_module)
|
|
elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then
|
|
plugin_module.path = plugin_root
|
|
plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin")
|
|
if disabled then
|
|
table.insert(self.disabled_plugins, plugin_module)
|
|
else
|
|
local ok_meta, plugin_metamodule = pcall(dofile, metafile)
|
|
if ok_meta and plugin_metamodule then
|
|
for k, module in pairs(plugin_metamodule) do
|
|
plugin_module[k] = module
|
|
end
|
|
end
|
|
sandboxPluginEventHandlers(plugin_module)
|
|
table.insert(self.enabled_plugins, plugin_module)
|
|
logger.dbg("Plugin loaded", plugin_module.name)
|
|
end
|
|
end
|
|
end
|
|
package.path = package_path
|
|
package.cpath = package_cpath
|
|
|
|
end
|
|
|
|
|
|
function PluginLoader:loadPlugins()
|
|
if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end
|
|
|
|
self.enabled_plugins = {}
|
|
self.disabled_plugins = {}
|
|
self.loaded_plugins = {}
|
|
|
|
local t = self:_discover()
|
|
table.sort(t, sortProvidersFirst)
|
|
self:_load(t)
|
|
-- set package path for all loaded plugins
|
|
for _, plugin in ipairs(self.enabled_plugins) do
|
|
package.path = string.format("%s;%s/?.lua", package.path, plugin.path)
|
|
package.cpath = string.format("%s;%s/lib/?.so", package.cpath, plugin.path)
|
|
end
|
|
|
|
table.sort(self.enabled_plugins, function(v1,v2) return v1.path < v2.path end)
|
|
|
|
return self.enabled_plugins, self.disabled_plugins
|
|
end
|
|
|
|
function PluginLoader:genPluginManagerSubItem()
|
|
if not self.all_plugins then
|
|
local enabled_plugins, disabled_plugins = self:loadPlugins()
|
|
self.all_plugins = {}
|
|
|
|
for _, plugin in ipairs(enabled_plugins) do
|
|
local element = getMenuTable(plugin)
|
|
element.enable = true
|
|
table.insert(self.all_plugins, element)
|
|
end
|
|
|
|
for _, plugin in ipairs(disabled_plugins) do
|
|
local element = getMenuTable(plugin)
|
|
element.enable = false
|
|
if not OBSOLETE_PLUGINS[element.name] then
|
|
table.insert(self.all_plugins, element)
|
|
end
|
|
end
|
|
|
|
table.sort(self.all_plugins, function(v1, v2) return v1.fullname < v2.fullname end)
|
|
end
|
|
|
|
local plugin_table = {}
|
|
for __, plugin in ipairs(self.all_plugins) do
|
|
table.insert(plugin_table, {
|
|
text = plugin.fullname,
|
|
checked_func = function()
|
|
return plugin.enable
|
|
end,
|
|
callback = function()
|
|
local UIManager = require("ui/uimanager")
|
|
local _ = require("gettext")
|
|
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled") or {}
|
|
plugin.enable = not plugin.enable
|
|
if plugin.enable then
|
|
plugins_disabled[plugin.name] = nil
|
|
else
|
|
plugins_disabled[plugin.name] = true
|
|
end
|
|
G_reader_settings:saveSetting("plugins_disabled", plugins_disabled)
|
|
if self.show_info then
|
|
self.show_info = false
|
|
UIManager:askForRestart()
|
|
end
|
|
end,
|
|
help_text = plugin.description,
|
|
})
|
|
end
|
|
return plugin_table
|
|
end
|
|
|
|
function PluginLoader:createPluginInstance(plugin, attr)
|
|
local ok, re = pcall(plugin.new, plugin, attr)
|
|
if ok then -- re is a plugin instance
|
|
self.loaded_plugins[plugin.name] = re
|
|
return ok, re
|
|
else -- re is the error message
|
|
logger.err("Failed to initialize", plugin.name, "plugin:", re)
|
|
return nil, re
|
|
end
|
|
end
|
|
|
|
--- Checks if a specific plugin is instantiated
|
|
function PluginLoader:isPluginLoaded(name)
|
|
return self.loaded_plugins[name] ~= nil
|
|
end
|
|
|
|
--- Returns the current instance of a specific Plugin (if any)
|
|
--- (NOTE: You can also usually access it via self.ui[plugin_name])
|
|
function PluginLoader:getPluginInstance(name)
|
|
return self.loaded_plugins[name]
|
|
end
|
|
|
|
-- *MUST* be called on destruction of whatever called createPluginInstance!
|
|
function PluginLoader:finalize()
|
|
-- Unpin stale references
|
|
self.loaded_plugins = {}
|
|
end
|
|
|
|
return PluginLoader
|