feat: Adds progress bar to cloud storage downloads (#13650)

This commit is contained in:
Linus045
2025-06-19 10:15:40 +02:00
committed by GitHub
parent e9e2de27c5
commit 3f19a2a05e
8 changed files with 246 additions and 21 deletions

View File

@@ -12,6 +12,7 @@ local LuaSettings = require("luasettings")
local Menu = require("ui/widget/menu")
local NetworkMgr = require("ui/network/manager")
local PathChooser = require("ui/widget/pathchooser")
local ProgressbarDialog = require("ui/widget/progressbardialog")
local UIManager = require("ui/uimanager")
local WebDav = require("apps/cloudstorage/webdav")
local lfs = require("libs/libkoreader-lfs")
@@ -213,19 +214,29 @@ end
function CloudStorage:downloadFile(item)
local function startDownloadFile(unit_item, address, username, password, path_dir, callback_close)
local progressbar_dialog = ProgressbarDialog:new {
title = _("Downloading…"),
subtitle = unit_item.text,
progress_max = unit_item.filesize,
}
UIManager:scheduleIn(1, function()
if self.type == "dropbox" then
DropBox:downloadFile(unit_item, password, path_dir, callback_close)
elseif self.type == "ftp" then
Ftp:downloadFile(unit_item, address, username, password, path_dir, callback_close)
elseif self.type == "webdav" then
WebDav:downloadFile(unit_item, address, username, password, path_dir, callback_close)
local progress_callback = function(progress)
progressbar_dialog:reportProgress(progress)
end
if self.type == "dropbox" then
DropBox:downloadFile(unit_item, password, path_dir, callback_close, progress_callback)
elseif self.type == "ftp" then
Ftp:downloadFile(unit_item, address, username, password, path_dir, callback_close, nil)
elseif self.type == "webdav" then
WebDav:downloadFile(unit_item, address, username, password, path_dir, callback_close, progress_callback)
end
progressbar_dialog:close()
end)
UIManager:show(InfoMessage:new{
text = _("Downloading. This might take a moment."),
timeout = 1,
})
progressbar_dialog:show()
end
local function createTitle(filename_orig, filesize, filename, path) -- title for ButtonDialog

View File

@@ -24,8 +24,8 @@ function DropBox:showFiles(url, password)
return DropBoxApi:showFiles(url, password)
end
function DropBox:downloadFile(item, password, path, callback_close)
local code_response = DropBoxApi:downloadFile(item.url, password, path)
function DropBox:downloadFile(item, password, path, callback_close, progress_callback)
local code_response = DropBoxApi:downloadFile(item.url, password, path, progress_callback)
if code_response == 200 then
local __, filename = util.splitFilePathName(path)
if G_reader_settings:isTrue("show_unsupported") and not DocumentRegistry:hasProvider(filename) then

View File

@@ -106,9 +106,13 @@ function DropBoxApi:fetchListFolders(path, token)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:downloadFile(path, token, local_path)
function DropBoxApi:downloadFile(path, token, local_path, progress_callback)
local data1 = "{\"path\": \"" .. path .. "\"}"
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local handle = ltn12.sink.file(io.open(local_path, "w"))
handle = socketutil.chainSinkWithProgressCallback(handle, progress_callback)
local code, headers, status = socket.skip(1, http.request{
url = API_DOWNLOAD_FILE,
method = "GET",
@@ -116,7 +120,7 @@ function DropBoxApi:downloadFile(path, token, local_path)
["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data1,
},
sink = ltn12.sink.file(io.open(local_path, "w")),
sink = handle,
})
socketutil:reset_timeout()
if code ~= 200 then
@@ -195,6 +199,7 @@ function DropBoxApi:listFolder(path, token, folder_mode)
table.insert(dropbox_file, {
text = text,
mandatory = util.getFriendlySize(files.size),
filesize = files.size,
url = files.path_display,
type = tag,
})
@@ -221,6 +226,7 @@ function DropBoxApi:listFolder(path, token, folder_mode)
mandatory = files.mandatory,
url = files.url,
type = files.type,
filesize = files.filesize,
})
end
return dropbox_list

View File

@@ -8,6 +8,7 @@ local ReaderUI = require("apps/reader/readerui")
local UIManager = require("ui/uimanager")
local ltn12 = require("ltn12")
local logger = require("logger")
local socketutil = require("socketutil")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
@@ -19,7 +20,7 @@ function Ftp:run(address, user, pass, path)
return FtpApi:listFolder(url, path)
end
function Ftp:downloadFile(item, address, user, pass, path, callback_close)
function Ftp:downloadFile(item, address, user, pass, path, callback_close, progress_callback)
local url = FtpApi:generateUrl(address, util.urlEncode(user), util.urlEncode(pass)) .. item.url
logger.dbg("downloadFile url", url)
path = util.fixUtf8(path, "_")
@@ -30,7 +31,11 @@ function Ftp:downloadFile(item, address, user, pass, path, callback_close)
})
return
end
local response = FtpApi:ftpGet(url, "retr", ltn12.sink.file(file))
local handle = ltn12.sink.file(file)
handle = socketutil.chainSinkWithProgressCallback(handle, progress_callback)
local response = FtpApi:ftpGet(url, "retr", handle)
if response ~= nil then
local __, filename = util.splitFilePathName(path)
if G_reader_settings:isTrue("show_unsupported") and not DocumentRegistry:hasProvider(filename) then

View File

@@ -17,8 +17,15 @@ function WebDav:run(address, user, pass, path, folder_mode)
return WebDavApi:listFolder(address, user, pass, path, folder_mode)
end
function WebDav:downloadFile(item, address, username, password, local_path, callback_close)
local code_response = WebDavApi:downloadFile(WebDavApi:getJoinedPath(address, item.url), username, password, local_path)
function WebDav:downloadFile(item, address, username, password, local_path, callback_close, progress_callback)
local code_response = WebDavApi:downloadFile(
WebDavApi:getJoinedPath(address, item.url),
username,
password,
local_path,
progress_callback
)
if code_response == 200 then
local __, filename = util.splitFilePathName(local_path)
if G_reader_settings:isTrue("show_unsupported") and not DocumentRegistry:hasProvider(filename) then

View File

@@ -162,13 +162,17 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
return webdav_list
end
function WebDavApi:downloadFile(file_url, user, pass, local_path)
function WebDavApi:downloadFile(file_url, user, pass, local_path, progress_callback)
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
logger.dbg("WebDavApi: downloading file: ", file_url)
local code, headers, status = socket.skip(1, http.request{
local handle = ltn12.sink.file(io.open(local_path, "w"))
handle = socketutil.chainSinkWithProgressCallback(handle, progress_callback)
local code, headers, status = socket.skip(1, http.request {
url = file_url,
method = "GET",
sink = ltn12.sink.file(io.open(local_path, "w")),
sink = handle,
user = user,
password = pass,
})

View File

@@ -172,4 +172,22 @@ function socketutil.redact_request(request)
return safe_request
end
function socketutil.chainSinkWithProgressCallback(sink, progressCallback)
if sink == nil or progressCallback == nil then
return sink
end
local downloaded_bytes = 0
local progress_reporter_filter = function(chunk, err)
if chunk ~= nil then
-- accumulate the downloaded bytes so we don't need to check the actual file every time
downloaded_bytes = downloaded_bytes + chunk:len()
progressCallback(downloaded_bytes)
end
return chunk, err
end
return ltn12.sink.chain(progress_reporter_filter, sink)
end
return socketutil

View File

@@ -0,0 +1,174 @@
--[[--
A dialog that shows a progress bar with a title and subtitle.
@usage
local progressbar_dialog = ProgressbarDialog:new {
title = nil,
subtitle = nil,
progress_max = nil
refresh_time_seconds = 3,
}
Note: provide at least one of title, subtitle or progress_max
@param title string the title of the dialog
@param subtitle string the subtitle of the dialog
@param progress_max number the maximum progress (e.g. size of the file in bytes for file downloads)
reportProgress() should be called with the current
progress (value between 0-progress_max) to update the progress bar
optional: if `progress_max` is nil, the progress bar will be hidden
@param refresh_time_seconds number refresh time in seconds
-- Attach progress callback and call show()
progressbar_dialog:show()
-- Call close() when download is done
progressbar_dialog:close()
-- To report progress, you can either:
-- manually call reportProgress with the current progress (value between 0-progress_max)
progressbar_dialog:reportProgress( <progress value> )
-- or when using luasocket sinks, chain the callback:
local sink = ltn12.sink.file(io.open(local_path, "w"))
sink = socketutil.chainSinkWithProgressCallback(sink, function(progress)
progressbar_dialog:reportProgress(progress)
end)
--]]
local Blitbuffer = require("ffi/blitbuffer")
local Device = require("device")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local ProgressWidget = require("ui/widget/progresswidget")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local dbg = require("dbg")
local time = require("ui/time")
local Screen = Device.screen
local ProgressbarDialog = WidgetContainer:extend {
refresh_time_seconds = 3,
}
function ProgressbarDialog:init()
self.align = "center"
self.dimen = Screen:getSize()
self.progress_bar_visible = self.progress_max ~= nil and self.progress_max > 0
-- used for internal state
self.last_redraw_time_ms = 0
-- create the dialog
local progress_bar_width = Screen:getWidth() - Screen:scaleBySize(80)
local progress_bar_height = Screen:scaleBySize(18)
-- only add relevant widgets
local vertical_group = VerticalGroup:new {}
if self.title then
vertical_group[#vertical_group + 1] = TextWidget:new {
text = self.title or "",
face = Font:getFace("ffont"),
bold = true,
max_width = progress_bar_width,
}
end
if self.subtitle then
vertical_group[#vertical_group + 1] = TextWidget:new {
text = self.subtitle or "",
face = Font:getFace("smallffont"),
max_width = progress_bar_width,
}
end
if self.progress_bar_visible then
self.progress_bar = ProgressWidget:new {
width = progress_bar_width,
height = progress_bar_height,
padding = Size.padding.large,
margin = Size.margin.tiny,
percentage = 0,
}
vertical_group[#vertical_group + 1] = self.progress_bar
end
self[1] = FrameContainer:new {
radius = Size.radius.window,
bordersize = Size.border.window,
padding = Size.padding.large,
background = Blitbuffer.COLOR_WHITE,
vertical_group
}
end
dbg:guard(ProgressbarDialog, "init",
nil,
function(self)
assert(self.progress_max == nil or
(type(self.progress_max) == "number" and self.progress_max > 0),
"Wrong self.progress_max type (expected nil or number greater than 0), value was: " ..
tostring(self.progress_max))
assert(type(self.refresh_time_seconds) == "number" and self.refresh_time_seconds > 0,
"Wrong self.refresh_time_seconds type (expected number greater than 0), value was: " ..
tostring(self.refresh_time_seconds))
assert(self.title == nil or type(self.title) == "string",
"Wrong title type (expected nil or string), value was of type: " .. type(self.title))
assert(self.subtitle == nil or type(self.subtitle) == "string",
"Wrong subtitle type (expected nil or string), value was of type: " .. type(self.subtitle))
assert(self.title or self.subtitle or self.progress_max,
"No values defined, dialog would be empty. Please provide at least one of title, subtitle or progress_max")
end)
--- Updates the UI to show the current percentage of the progress bar when needed.
function ProgressbarDialog:redrawProgressbarIfNeeded()
-- grab the current percentage from the progress bar
local current_percentage = self.progress_bar.percentage
-- if we are at 100% always redraw
if current_percentage >= 1 then
self:redrawProgressbar()
return
end
-- check if enough time has passed
local current_time_ms = time.now()
local time_delta_ms = current_time_ms - self.last_redraw_time_ms
local refresh_time_ms = self.refresh_time_seconds * 1000 * 1000
if time_delta_ms >= refresh_time_ms then
self.last_redraw_time_ms = current_time_ms
self:redrawProgressbar()
end
end
function ProgressbarDialog:redrawProgressbar()
--UI is not updating during file download so force an update
UIManager:setDirty(self, function() return "fast", self.progress_bar.dimen end)
UIManager:forceRePaint()
end
--- Used to notify about a progress update.
-- @param progress number the current progress (e.g. size of the file in bytes for file downloads)
function ProgressbarDialog:reportProgress(progress)
if not self.progress_bar_visible then
return
end
-- set percentage of progress bar internally, this does not yet update the screen element
self.progress_bar:setPercentage(progress / self.progress_max)
-- actually draw the progress bar update
self:redrawProgressbarIfNeeded()
end
--- Opens dialog.
function ProgressbarDialog:show()
UIManager:show(self, "ui")
end
---- Closes dialog.
function ProgressbarDialog:close()
UIManager:close(self, "ui")
end
return ProgressbarDialog