mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
Added Trapper module, for simple interaction with UI
This module provides methods for simple interaction with UI, without the need for explicit callbacks, for use by linear jobs between their steps. Uses coroutines, but their usage is hidden by a simple API. Factored out of Wikipedia:createEpubWithUI().
This commit is contained in:
278
frontend/ui/trapper.lua
Normal file
278
frontend/ui/trapper.lua
Normal file
@@ -0,0 +1,278 @@
|
||||
--[[--
|
||||
Trapper module: provides methods for simple interaction with UI,
|
||||
without the need for explicit callbacks, for use by linear jobs
|
||||
between their steps.
|
||||
|
||||
Allows code to trap UI (give progress info to UI, ask for user choice),
|
||||
or get trapped by UI (get interrupted).
|
||||
Mostly done with coroutines, but hides their usage for simplicity.
|
||||
]]
|
||||
|
||||
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
local _ = require("gettext")
|
||||
|
||||
local Trapper = {}
|
||||
|
||||
--[[--
|
||||
Executes a function and allows it to be trapped (that is: to use our
|
||||
other methods).
|
||||
|
||||
Simple wrapper function for a coroutine, which is a prerequisite
|
||||
for all our methods (this simply abstracts the @{coroutine}
|
||||
business to our callers), and execute it.
|
||||
|
||||
(If some code is not wrap()'ed, most of the other methods, when called,
|
||||
will simply log or fallback to a non-UI action or OK choice.)
|
||||
|
||||
This call should be the last step in some event processing code,
|
||||
as it may return early (the first @{coroutine.yield|coroutine.yield()} in any of the other
|
||||
methods will return from this function), and later be resumed by @{ui.uimanager|UIManager}.
|
||||
So any following (unwrapped) code would be then executed while `func`
|
||||
is half-done, with unintended consequences.
|
||||
|
||||
@param func function reference to function to wrap and execute
|
||||
]]
|
||||
function Trapper:wrap(func)
|
||||
-- Catch and log any error happening in func (an error happening
|
||||
-- in a coroutine just aborts silently the coroutine)
|
||||
local pcalled_func = function()
|
||||
-- we use xpcall as it can give a whole stacktrace, unlike pcall
|
||||
local ok, err = xpcall(func, debug.traceback)
|
||||
if not ok then
|
||||
logger.warn("error in wrapped function:", err)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
-- As a coroutine, we will return at first coroutine.yield(),
|
||||
-- and the above true/false won't probably be caught by
|
||||
-- any code, but let's do it anyway.
|
||||
end
|
||||
local co = coroutine.create(pcalled_func)
|
||||
return coroutine.resume(co)
|
||||
end
|
||||
|
||||
--- Returns if code is wrapped
|
||||
--
|
||||
-- @treturn boolean true if code is wrapped by Trapper, false otherwise
|
||||
function Trapper:isWrapped()
|
||||
if coroutine.running() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Clears left-over widget
|
||||
function Trapper:clear()
|
||||
if self:isWrapped() then
|
||||
if self.current_widget then
|
||||
UIManager:close(self.current_widget)
|
||||
UIManager:forceRePaint()
|
||||
self.current_widget = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Clears left-over widget and resets Trapper state
|
||||
function Trapper:reset()
|
||||
self:clear()
|
||||
-- Reset some properties
|
||||
self.paused_text = nil
|
||||
self.paused_continue_text = nil
|
||||
self.paused_abort_text = nil
|
||||
return true
|
||||
end
|
||||
|
||||
--[[--
|
||||
Displays an InfoMessage, and catches dismissal.
|
||||
|
||||
Display a InfoMessage with text, or keep existing InfoMessage if text = nil,
|
||||
and return true.
|
||||
|
||||
UNLESS the previous widget was itself a InfoMessage and it has been
|
||||
dismissed (by Tap on the screen), in which case the new InfoMessage
|
||||
is not displayed, and false is returned.
|
||||
|
||||
One can only know a InfoMessage has been dismissed when trying to
|
||||
display a new one (we can't do better than that with coroutines).
|
||||
So, don't hesitate to call it regularly (each call costs 100ms), between
|
||||
steps of the work, to provide good responsiveness.
|
||||
|
||||
Trapper:info() is a shortcut to get dismiss info while keeping
|
||||
the existing InfoMessage displayed.
|
||||
|
||||
@string text text to display as an InfoMessage (or nil to keep existing one)
|
||||
@treturn boolean true if InfoMessage was not dismissed, false if dismissed
|
||||
|
||||
@usage
|
||||
Trapper:info("some text about step or progress")
|
||||
go_on = Trapper:info()
|
||||
]]
|
||||
function Trapper:info(text)
|
||||
local _coroutine = coroutine.running()
|
||||
if not _coroutine then
|
||||
logger.info("unwrapped info:", text)
|
||||
return true -- not dismissed
|
||||
end
|
||||
|
||||
if self.current_widget and self.current_widget.is_infomessage then
|
||||
-- We are replacing a InfoMessage with a new InfoMessage: we want to check
|
||||
-- if the previous one was dismissed.
|
||||
-- We added a dismiss_callback to our previous InfoMessage. For a Tap
|
||||
-- to get processed and get our dismiss_callback called, we need to give
|
||||
-- control for a short time to UIManager: this will be done with
|
||||
-- the coroutine.yield() that follows.
|
||||
-- If no dismiss_callback was fired, we need to get this code resumed:
|
||||
-- that will be done with the following go_on_func schedule in 0.1 second.
|
||||
local go_on_func = function() coroutine.resume(_coroutine, true) end
|
||||
-- delay matters: 0.05 or 0.1 seems fine
|
||||
-- 0.01 is too fast: go_on_func is called before our dismiss_callback is processed
|
||||
UIManager:scheduleIn(0.1, go_on_func)
|
||||
|
||||
local go_on = coroutine.yield() -- gives control back to UIManager
|
||||
-- go_on is the 2nd arg given to the coroutine.resume() that got us resumed:
|
||||
-- false if it was a dismiss_callback
|
||||
-- true if it was the schedule go_on_func
|
||||
|
||||
if not go_on then -- dismiss_callback called
|
||||
UIManager:unschedule(go_on_func) -- no more need for this scheduled action
|
||||
-- Don't just return false without confirmation (this tap may have been
|
||||
-- made by error, and we don't want to just cancel a long running job)
|
||||
local abort_box = ConfirmBox:new{
|
||||
text = self.paused_text and self.paused_text or _("Paused"),
|
||||
-- ok and cancel reversed, as tapping outside will
|
||||
-- get cancel_callback called: if tap outside was the
|
||||
-- result of a tap error, we want to continue. Cancelling
|
||||
-- will need an explicit tap on the ok_text button.
|
||||
cancel_text = self.paused_continue_text and self.paused_continue_text or _("Continue"),
|
||||
ok_text = self.paused_abort_text and self.paused_abort_text or _("Abort"),
|
||||
cancel_callback = function()
|
||||
coroutine.resume(_coroutine, true)
|
||||
end,
|
||||
ok_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
}
|
||||
UIManager:show(abort_box)
|
||||
-- no need to forceRePaint, UIManager will do it when we yield()
|
||||
go_on = coroutine.yield() -- abort_box ok/cancel from their coroutine.resume()
|
||||
UIManager:close(abort_box)
|
||||
if not go_on then
|
||||
UIManager:close(self.current_widget)
|
||||
UIManager:forceRePaint()
|
||||
return false
|
||||
end
|
||||
if self.current_widget then
|
||||
-- Re-show current widget that was dismissed
|
||||
-- (this is fine for our simple InfoMessage)
|
||||
UIManager:show(self.current_widget)
|
||||
end
|
||||
UIManager:forceRePaint()
|
||||
end
|
||||
-- go_on_func returned result = true, or abort_box did not abort:
|
||||
-- continue processing
|
||||
end
|
||||
|
||||
-- TODO We should try to flush any pending tap, so past
|
||||
-- events won't be considered action on the yet to be displayed
|
||||
-- widget
|
||||
|
||||
-- We're going to display a new widget, close previous one
|
||||
if self.current_widget then
|
||||
UIManager:close(self.current_widget)
|
||||
-- no repaint here, we'll do that below when a new one is shown
|
||||
end
|
||||
|
||||
-- dismiss_callback will be checked for at start of next call
|
||||
self.current_widget = InfoMessage:new{
|
||||
text = text,
|
||||
dismiss_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
is_infomessage = true -- flag on our InfoMessages
|
||||
}
|
||||
logger.dbg("Showing InfoMessage:", text)
|
||||
UIManager:show(self.current_widget)
|
||||
UIManager:forceRePaint()
|
||||
return true
|
||||
end
|
||||
|
||||
--[[--
|
||||
Overrides text and button texts on the Paused ConfirmBox.
|
||||
|
||||
A ConfirmBox is displayed when an InfoMessage is dismissed
|
||||
in Trapper:info(), with default text "Paused", and default
|
||||
buttons "Abort" and "Continue".
|
||||
|
||||
@string text ConfirmBox text (default: "Paused")
|
||||
@string abort_text ConfirmBox "Abort" button text (Trapper:info() returns false)
|
||||
@string continue_text ConfirmBox "Continue" button text
|
||||
]]
|
||||
function Trapper:setPausedText(text, abort_text, continue_text)
|
||||
if self:isWrapped() then
|
||||
self.paused_text = text
|
||||
self.paused_abort_text = abort_text
|
||||
self.paused_continue_text = continue_text
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[--
|
||||
Displays a ConfirmBox and gets user's choice.
|
||||
|
||||
Display a ConfirmBox with the text and cancel_text/ok_text buttons,
|
||||
block and wait for user's choice, and return the choice made:
|
||||
false if Cancel tapped or dismissed, true if OK tapped
|
||||
|
||||
@string text text to display in a ConfirmBox
|
||||
@string cancel_text text for ConfirmBox Cancel button
|
||||
@string ok_text text for ConfirmBox Ok button
|
||||
@treturn boolean false if Cancel tapped or dismissed, true if OK tapped
|
||||
|
||||
@usage
|
||||
go_on = Trapper:confirm("Do you want to go on?")
|
||||
that_selected = Trapper:confirm("Do you want to do this or that?", "this", "that"))
|
||||
]]
|
||||
function Trapper:confirm(text, cancel_text, ok_text)
|
||||
-- With ConfirmBox, Cancel button is on the left, OK button on the right,
|
||||
-- so buttons order is consistent with this function args
|
||||
local _coroutine = coroutine.running()
|
||||
if not _coroutine then
|
||||
logger.info("unwrapped confirm, returning true to:", text)
|
||||
return true -- always select "OK" in ConfirmBox if no UI
|
||||
end
|
||||
|
||||
-- TODO We should try to flush any pending tap, so past
|
||||
-- events won't be considered action on the yet to be displayed
|
||||
-- widget
|
||||
|
||||
-- Close any previous widget
|
||||
if self.current_widget then
|
||||
UIManager:close(self.current_widget)
|
||||
-- no repaint here, we'll do that below when a new one is shown
|
||||
end
|
||||
|
||||
-- We will yield(), and both callbacks will resume() us
|
||||
self.current_widget = ConfirmBox:new{
|
||||
text = text,
|
||||
ok_text = ok_text,
|
||||
cancel_text = cancel_text,
|
||||
cancel_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
ok_callback = function()
|
||||
coroutine.resume(_coroutine, true)
|
||||
end,
|
||||
}
|
||||
logger.dbg("Showing ConfirmBox and waiting for answer:", text)
|
||||
UIManager:show(self.current_widget)
|
||||
-- no need to forceRePaint, UIManager will do it when we yield()
|
||||
local ret = coroutine.yield() -- wait for ConfirmBox callback
|
||||
logger.dbg("ConfirmBox answers", ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
return Trapper
|
||||
@@ -286,25 +286,19 @@ local ext_to_mimetype = {
|
||||
|
||||
|
||||
-- Create an epub file (with possibly images)
|
||||
-- This is non-UI code (for batch creation or emulator test), but it accepts
|
||||
-- a progress_callback function that will be feed with progress information
|
||||
-- that could be shown to the user.
|
||||
function Wikipedia:createEpub(epub_path, page, lang, with_images, progress_callback)
|
||||
if not progress_callback then
|
||||
-- Make our own logging only process_callback
|
||||
progress_callback = function(text, confirm)
|
||||
logger.info("progress", confirm and "confirm" or "info", text)
|
||||
return true -- always select "OK" in ConfirmBox
|
||||
end
|
||||
end
|
||||
function Wikipedia:createEpub(epub_path, page, lang, with_images)
|
||||
-- Use Trapper to display progress and ask questions through the UI.
|
||||
-- We need to have been Trapper.wrap()'ed for UI to be used, otherwise
|
||||
-- Trapper:info() and Trapper:confirm() will just use logger.
|
||||
local UI = require("ui/trapper")
|
||||
|
||||
progress_callback(_("Fetching Wikipedia page…"))
|
||||
UI:info(_("Fetching Wikipedia page…"))
|
||||
local ok, phtml = pcall(self.wikiphtml, self, page, lang)
|
||||
if not ok then
|
||||
progress_callback(phtml)
|
||||
UI:info(phtml) -- display error in InfoMessage
|
||||
-- Sleep a bit to make that error seen
|
||||
util.sleep(2)
|
||||
progress_callback() -- close last progress info
|
||||
UI:reset()
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -407,14 +401,14 @@ function Wikipedia:createEpub(epub_path, page, lang, with_images, progress_callb
|
||||
local include_images = false
|
||||
local use_img_2x = false
|
||||
if with_images then
|
||||
-- if no progress_callback (non UI), our fake one will return true
|
||||
-- If no UI (Trapper:wrap() not called), UI:confirm() will answer true
|
||||
if #images > 0 then
|
||||
include_images = progress_callback(T(_("The page contains %1 images.\nWould you like to download and include them in the generated EPUB file?"), #images), true, _("Include"), _("Don't include"))
|
||||
include_images = UI:confirm(T(_("The page contains %1 images.\nWould you like to download and include them in the generated EPUB file?"), #images), _("Don't include"), _("Include"))
|
||||
if include_images then
|
||||
use_img_2x = progress_callback(_("Would you like to use slightly higher quality images? This will result in a bigger file size."), true, _("Higher quality"), _("Standard quality"))
|
||||
use_img_2x = UI:confirm(_("Would you like to use slightly higher quality images? This will result in a bigger file size."), _("Standard quality"), _("Higher quality"))
|
||||
end
|
||||
else
|
||||
progress_callback(_("The page does not contain any images."))
|
||||
UI:info(_("The page does not contain any images."))
|
||||
util.sleep(1) -- Let the user see that
|
||||
end
|
||||
end
|
||||
@@ -427,6 +421,7 @@ function Wikipedia:createEpub(epub_path, page, lang, with_images, progress_callb
|
||||
-- the images he chose to not get.
|
||||
end
|
||||
|
||||
UI:info(_("Building EPUB…"))
|
||||
-- Open the zip file (with .tmp for now, as crengine may still
|
||||
-- have a handle to the final epub_path, and we don't want to
|
||||
-- delete a good one if we fail/cancel later)
|
||||
@@ -759,7 +754,7 @@ time, abbr, sup {
|
||||
for inum, img in ipairs(images) do
|
||||
-- Process can be interrupted at this point between each image download
|
||||
-- by tapping while the InfoMessage is displayed
|
||||
local go_on = progress_callback(T(_("Fetching image %1 / %2 …"), inum, nb_images))
|
||||
local go_on = UI:info(T(_("Fetching image %1 / %2 …"), inum, nb_images))
|
||||
if not go_on then
|
||||
cancelled = true
|
||||
break
|
||||
@@ -779,7 +774,7 @@ time, abbr, sup {
|
||||
if success then
|
||||
epub:add("OEBPS/"..img.imgpath, content)
|
||||
else
|
||||
go_on = progress_callback(T(_("Downloading image %1 failed. Continue anyway?"), inum), true, _("Continue"), _("Stop"))
|
||||
go_on = UI:confirm(T(_("Downloading image %1 failed. Continue anyway?"), inum), _("Stop"), _("Continue"))
|
||||
if not go_on then
|
||||
cancelled = true
|
||||
break
|
||||
@@ -790,19 +785,19 @@ time, abbr, sup {
|
||||
|
||||
-- Done with adding files
|
||||
if cancelled then
|
||||
if progress_callback(_("Download did not complete.\nDo you want to create an EPUB with the already downloaded images?"), true, _("Create"), _("Don't create")) then
|
||||
if UI:confirm(_("Download did not complete.\nDo you want to create an EPUB with the already downloaded images?"), _("Don't create"), _("Create")) then
|
||||
cancelled = false
|
||||
end
|
||||
end
|
||||
if cancelled then
|
||||
progress_callback(_("Canceled. Cleaning up…"))
|
||||
UI:info(_("Canceled. Cleaning up…"))
|
||||
else
|
||||
progress_callback(_("Packing EPUB…"))
|
||||
UI:info(_("Packing EPUB…"))
|
||||
end
|
||||
epub:close()
|
||||
-- This was nearly a no-op, so sleep a bit to make that progress step seen
|
||||
util.usleep(300000)
|
||||
progress_callback() -- close last progress info
|
||||
UI:reset() -- close last InfoMessage
|
||||
|
||||
if cancelled then
|
||||
-- Build was cancelled, remove half created .epub
|
||||
@@ -819,132 +814,29 @@ time, abbr, sup {
|
||||
end
|
||||
|
||||
|
||||
-- Wrapper to Wikipedia:createEpub() with UI progress info
|
||||
-- Wrap Wikipedia:createEpub() with UI progress info, provided
|
||||
-- by Trapper module.
|
||||
function Wikipedia:createEpubWithUI(epub_path, page, lang, result_callback)
|
||||
-- For progress_callback to be able to wait when needed
|
||||
-- for user confirmation, we need to wrap Wikipedia:createEpub
|
||||
-- in a coroutine, that can be resumed by these confirm callbacks.
|
||||
local UIManager = require("ui/uimanager")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
|
||||
-- Visual progress callback
|
||||
local cur_progress_box = nil
|
||||
local function ui_progress_callback(text, confirmbox, ok_text, cancel_text)
|
||||
if cur_progress_box then
|
||||
-- We want to catch a tap outside an InfoMessage (that the user
|
||||
-- could use to abort downloading) which will have its dismiss_callback
|
||||
-- called. For it to get a chance to get processed, we need to give
|
||||
-- control back to UIManager: that will be done with the coroutine.yield()
|
||||
-- that follows. If no dismiss_callback fired, we need to get this code resumed,
|
||||
-- and that will be done with the following go_on_func schedule in 0.1 second.
|
||||
local _coroutine = coroutine.running()
|
||||
local go_on_func = function() coroutine.resume(_coroutine, true) end
|
||||
-- delay matters: 0.05 or 0.1 seems fine
|
||||
-- 0.01 is too fast: go_on_func is called before our dismiss_callback is processed
|
||||
UIManager:scheduleIn(0.1, go_on_func)
|
||||
if coroutine.running() then
|
||||
-- Gives control back to UIManager, and get the 2nd arg given to the
|
||||
-- coroutine.resume() that got us resumed (either dismiss_callback or go_on_func)
|
||||
local result = coroutine.yield()
|
||||
if not result then -- dismiss_callback called
|
||||
UIManager:unschedule(go_on_func)
|
||||
local abort_box = ConfirmBox:new{
|
||||
text = _("Download paused"),
|
||||
-- ok and cancel reversed, as tapping outside will
|
||||
-- get cancel_callback called: if tap outside was the
|
||||
-- result of a tap error, we want to continue. Cancelling
|
||||
-- will need an explicit tap on the ok_text button.
|
||||
ok_text = _("Abort"),
|
||||
cancel_text = _("Continue"),
|
||||
ok_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
cancel_callback = function()
|
||||
coroutine.resume(_coroutine, true)
|
||||
end,
|
||||
}
|
||||
UIManager:show(abort_box)
|
||||
UIManager:forceRePaint()
|
||||
if coroutine.running() then
|
||||
result = coroutine.yield() -- abort_box result
|
||||
end
|
||||
if not result then
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- go_on_func returned result = true, or abort_box did not abort:
|
||||
-- continue processing
|
||||
end
|
||||
|
||||
-- close previous progress info
|
||||
UIManager:close(cur_progress_box)
|
||||
-- no repaint here, we'll do that below when new stuff is shown
|
||||
end
|
||||
if not text then
|
||||
-- no text given, used to just close previous progress info when done
|
||||
-- a repaint is needed
|
||||
UIManager:forceRePaint()
|
||||
return true
|
||||
end
|
||||
if confirmbox then
|
||||
-- ConfirmBox requested: callbacks will resume coroutine
|
||||
local _coroutine = coroutine.running()
|
||||
cur_progress_box = ConfirmBox:new{
|
||||
text = text,
|
||||
ok_text = ok_text,
|
||||
cancel_text = cancel_text,
|
||||
ok_callback = function()
|
||||
coroutine.resume(_coroutine, true)
|
||||
end,
|
||||
cancel_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
}
|
||||
else
|
||||
-- simple InfoMessage requested: dismiss callback
|
||||
-- will be checked for at start of next call
|
||||
local _coroutine = coroutine.running()
|
||||
cur_progress_box = InfoMessage:new{
|
||||
text = text,
|
||||
dismiss_callback = function()
|
||||
coroutine.resume(_coroutine, false)
|
||||
end,
|
||||
}
|
||||
end
|
||||
logger.dbg("Showing", confirmbox and "ConfirmBox" or "InfoMessage", text)
|
||||
UIManager:show(cur_progress_box)
|
||||
UIManager:forceRePaint()
|
||||
if not confirmbox then
|
||||
return true -- nothing more to do
|
||||
end
|
||||
-- we need to wait for ConfirmBox callback
|
||||
logger.dbg("waiting for coroutine to resume")
|
||||
if coroutine.running() then
|
||||
local result = coroutine.yield()
|
||||
logger.dbg(" coroutine ran and returned", result)
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Coroutine wrapping Wikipedia:createEpub()
|
||||
local co = coroutine.create(function()
|
||||
-- If errors in Wikipedia:createEpub(), the coroutine
|
||||
-- would just abort without crashing the reader, so
|
||||
-- pcall would not be needed. But if that happens,
|
||||
-- pcall will let us know and returns the error,
|
||||
-- that we can log.
|
||||
local ok, success = pcall(self.createEpub, self, epub_path, page, lang, true, ui_progress_callback)
|
||||
-- To do any UI interaction while building the EPUB, we need
|
||||
-- to use a coroutine, so that our code can be suspended while waiting
|
||||
-- for user interaction, and resumed by UI widgets callbacks.
|
||||
-- All this is hidden and done by Trapper with a simple API.
|
||||
local Trapper = require("ui/trapper")
|
||||
Trapper:wrap(function()
|
||||
Trapper:setPausedText("Download paused")
|
||||
-- If errors in Wikipedia:createEpub(), the coroutine (used by
|
||||
-- Trapper) would just abort (no reader crash, no error logged).
|
||||
-- So we use pcall to catch any errors, log it, and report
|
||||
-- the failure via result_callback.
|
||||
local ok, success = pcall(self.createEpub, self, epub_path, page, lang, true)
|
||||
if ok and success then
|
||||
result_callback(true)
|
||||
else
|
||||
ui_progress_callback() -- close any last progress info not cleaned
|
||||
Trapper:reset() -- close any last widget not cleaned if error
|
||||
logger.warn("Wikipedia.createEpub pcall:", ok, success)
|
||||
result_callback(false)
|
||||
end
|
||||
end)
|
||||
-- Execute coroutine
|
||||
coroutine.resume(co)
|
||||
end
|
||||
|
||||
return Wikipedia
|
||||
|
||||
Reference in New Issue
Block a user