mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(koreader): align download dialog preview with device path
Ensure the catalog download dialog previews and resolves the destination folder and filename from devicePath when configured. Fixes #752
This commit is contained in:
@@ -38,6 +38,30 @@ local function sanitizeDevicePath(device_path)
|
||||
return table.concat(segments, "/")
|
||||
end
|
||||
|
||||
local function joinDownloadPath(download_dir, relative_path)
|
||||
return (download_dir ~= "/" and download_dir or "") .. "/" .. relative_path
|
||||
end
|
||||
|
||||
local function resolveRelativeDownloadPath(download_dir, filename, filetype, device_path, filename_override)
|
||||
local relative = sanitizeDevicePath(device_path)
|
||||
if relative and not filename_override then return relative end
|
||||
|
||||
local parent = relative and relative:match("^(.*)/[^/]+$")
|
||||
local safe_filename = util.getSafeFilename(filename .. "." .. string.lower(filetype or "bin"), download_dir)
|
||||
return parent and parent ~= "" and parent .. "/" .. safe_filename or safe_filename
|
||||
end
|
||||
|
||||
local function splitDownloadPreview(download_dir, relative_path, filetype)
|
||||
local parent, filename = relative_path:match("^(.*)/([^/]+)$")
|
||||
local folder = parent and parent ~= "" and joinDownloadPath(download_dir, parent) or download_dir
|
||||
filename = filename or relative_path
|
||||
local extension = "." .. string.lower(filetype or "bin")
|
||||
if filename:sub(-#extension):lower() == extension then
|
||||
filename = filename:sub(1, #filename - #extension)
|
||||
end
|
||||
return folder, filename
|
||||
end
|
||||
|
||||
function CatalogDownload.install(Catalog)
|
||||
function Catalog:showFileChoices(detail)
|
||||
local files = self:supportedFiles(detail)
|
||||
@@ -88,17 +112,18 @@ function CatalogDownload.install(Catalog)
|
||||
return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir")
|
||||
end
|
||||
|
||||
function Catalog:getLocalDownloadPath(filename, filetype, device_path)
|
||||
function Catalog:getLocalDownloadPath(filename, filetype, device_path, filename_override)
|
||||
local download_dir = self:getCurrentDownloadDir()
|
||||
local relative = sanitizeDevicePath(device_path)
|
||||
if relative then
|
||||
local parent = relative:match("^(.*)/[^/]+$")
|
||||
if parent and parent ~= "" then util.makePath(download_dir .. "/" .. parent) end
|
||||
return (download_dir ~= "/" and download_dir or "") .. "/" .. relative
|
||||
end
|
||||
filename = filename .. "." .. string.lower(filetype or "bin")
|
||||
filename = util.getSafeFilename(filename, download_dir)
|
||||
return (download_dir ~= "/" and download_dir or "") .. "/" .. filename
|
||||
local relative = resolveRelativeDownloadPath(download_dir, filename, filetype, device_path, filename_override)
|
||||
local parent = relative:match("^(.*)/[^/]+$")
|
||||
if parent and parent ~= "" then util.makePath(joinDownloadPath(download_dir, parent)) end
|
||||
return joinDownloadPath(download_dir, relative)
|
||||
end
|
||||
|
||||
function Catalog:getLocalDownloadPreview(filename, filetype, device_path, filename_override)
|
||||
local download_dir = self:getCurrentDownloadDir()
|
||||
local relative = resolveRelativeDownloadPath(download_dir, filename, filetype, device_path, filename_override)
|
||||
return splitDownloadPreview(download_dir, relative, filetype)
|
||||
end
|
||||
|
||||
function Catalog:downloadDefaultFile(detail, file)
|
||||
@@ -141,9 +166,13 @@ function CatalogDownload.install(Catalog)
|
||||
UIManager:show(dialog)
|
||||
end
|
||||
|
||||
function Catalog:showDownloadDialog(detail, file)
|
||||
local filename = safeFilenameBase(detail)
|
||||
function Catalog:showDownloadDialog(detail, file, initial_filename_override)
|
||||
local filename = initial_filename_override or safeFilenameBase(detail)
|
||||
local filetype = string.lower(file.format or "bin")
|
||||
local filename_overridden = initial_filename_override ~= nil
|
||||
|
||||
local folder
|
||||
folder, filename = self:getLocalDownloadPreview(filename, filetype, file.devicePath, filename_overridden)
|
||||
|
||||
local function createTitle(path, name)
|
||||
return T(_("Download folder:\n%1\n\nDownload filename:\n%2\n\nDownload file type:\n%3"),
|
||||
@@ -152,14 +181,14 @@ function CatalogDownload.install(Catalog)
|
||||
|
||||
local dialog
|
||||
dialog = ButtonDialog:new{
|
||||
title = createTitle(self:getCurrentDownloadDir(), filename),
|
||||
title = createTitle(folder, filename),
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Download"),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
local local_path = self:getLocalDownloadPath(filename, filetype, file.devicePath)
|
||||
local local_path = self:getLocalDownloadPath(filename, filetype, file.devicePath, filename_overridden)
|
||||
self:checkDownloadFile(local_path, detail, file)
|
||||
end,
|
||||
},
|
||||
@@ -179,7 +208,7 @@ function CatalogDownload.install(Catalog)
|
||||
end
|
||||
G_reader_settings:saveSetting("download_dir", path)
|
||||
UIManager:nextTick(function()
|
||||
self:showDownloadDialog(detail, file)
|
||||
self:showDownloadDialog(detail, file, filename_overridden and filename or nil)
|
||||
end)
|
||||
end,
|
||||
}:chooseDir(self:getCurrentDownloadDir())
|
||||
@@ -206,9 +235,12 @@ function CatalogDownload.install(Catalog)
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
local value = util.trim(input_dialog:getInputText() or "")
|
||||
if value ~= "" then filename = value end
|
||||
if value ~= "" then
|
||||
filename_overridden = true
|
||||
folder, filename = self:getLocalDownloadPreview(value, filetype, file.devicePath, true)
|
||||
end
|
||||
UIManager:close(input_dialog)
|
||||
dialog:setTitle(createTitle(self:getCurrentDownloadDir(), filename))
|
||||
dialog:setTitle(createTitle(folder, filename))
|
||||
end,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ local BookOrbitProgressSync = require("bookorbit_progress_sync")
|
||||
local BookOrbitSweep = require("bookorbit_sweep")
|
||||
local BookOrbitUpdater = require("bookorbit_updater")
|
||||
|
||||
local PLUGIN_VERSION = "1.3.0"
|
||||
local PLUGIN_VERSION = "1.3.1"
|
||||
|
||||
local SYNC_STRATEGY = {
|
||||
PROMPT = 1,
|
||||
|
||||
@@ -55,8 +55,15 @@ package.loaded["logger"] = {
|
||||
warn = function() end,
|
||||
}
|
||||
package.loaded["ffi/util"] = {
|
||||
template = function(text)
|
||||
return text
|
||||
template = function(value, ...)
|
||||
local result = value
|
||||
for index = 1, select("#", ...) do
|
||||
local replacement = tostring(select(index, ...))
|
||||
result = result:gsub("%%" .. index, function()
|
||||
return replacement
|
||||
end)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
}
|
||||
local made_paths = {}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
local shown_widget
|
||||
local typed_filename
|
||||
local made_paths = {}
|
||||
local scheduled = {}
|
||||
|
||||
local function identity(value)
|
||||
return value
|
||||
end
|
||||
|
||||
package.loaded["ui/bidi"] = {
|
||||
dirpath = identity,
|
||||
filepath = identity,
|
||||
}
|
||||
package.loaded["ui/widget/buttondialog"] = {
|
||||
new = function(_, opts)
|
||||
function opts:setTitle(title)
|
||||
self.title = title
|
||||
end
|
||||
return opts
|
||||
end,
|
||||
}
|
||||
package.loaded["ui/widget/inputdialog"] = {
|
||||
new = function(_, opts)
|
||||
function opts:getInputText()
|
||||
return typed_filename or self.input
|
||||
end
|
||||
function opts:onShowKeyboard() end
|
||||
return opts
|
||||
end,
|
||||
}
|
||||
package.loaded["ui/widget/confirmbox"] = package.loaded["ui/widget/buttondialog"]
|
||||
package.loaded["ui/widget/infomessage"] = package.loaded["ui/widget/buttondialog"]
|
||||
package.loaded["ui/widget/notification"] = package.loaded["ui/widget/buttondialog"]
|
||||
package.loaded["ui/uimanager"] = {
|
||||
show = function(_, widget)
|
||||
shown_widget = widget
|
||||
end,
|
||||
close = function() end,
|
||||
nextTick = function(_, callback)
|
||||
table.insert(scheduled, callback)
|
||||
end,
|
||||
forceRePaint = function() end,
|
||||
}
|
||||
package.loaded["libs/libkoreader-lfs"] = {
|
||||
attributes = function()
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
package.loaded["logger"] = {
|
||||
dbg = function() end,
|
||||
warn = function() end,
|
||||
}
|
||||
package.loaded["ffi/util"] = {
|
||||
template = function(value, ...)
|
||||
local result = value
|
||||
for index = 1, select("#", ...) do
|
||||
local replacement = tostring(select(index, ...))
|
||||
result = result:gsub("%%" .. index, function()
|
||||
return replacement
|
||||
end)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
}
|
||||
package.loaded["util"] = {
|
||||
makePath = function(path)
|
||||
table.insert(made_paths, path)
|
||||
end,
|
||||
getSafeFilename = function(filename)
|
||||
return filename:gsub("[\\/:*?\"<>|]", "_")
|
||||
end,
|
||||
trim = function(value)
|
||||
return tostring(value or ""):match("^%s*(.-)%s*$")
|
||||
end,
|
||||
}
|
||||
package.loaded["bookorbit_state"] = {}
|
||||
package.loaded["gettext"] = identity
|
||||
package.loaded["bookorbit_catalog_util"] = {
|
||||
formatBytes = tostring,
|
||||
safeFilenameBase = function(detail)
|
||||
return detail.fallbackFilename
|
||||
end,
|
||||
}
|
||||
|
||||
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
||||
|
||||
local CatalogDownload = require("bookorbit_catalog_download")
|
||||
local Catalog = {}
|
||||
CatalogDownload.install(Catalog)
|
||||
|
||||
local function assertEqual(actual, expected, label)
|
||||
if actual ~= expected then
|
||||
error(string.format("%s: expected %s, got %s", label, tostring(expected), tostring(actual)))
|
||||
end
|
||||
end
|
||||
|
||||
local function assertContains(value, expected, label)
|
||||
if not value:find(expected, 1, true) then
|
||||
error(string.format("%s: expected %q in %q", label, expected, value))
|
||||
end
|
||||
end
|
||||
|
||||
local download_dir = "/mnt/onboard/library"
|
||||
G_reader_settings = {
|
||||
readSetting = function(_, key)
|
||||
if key == "download_dir" then return download_dir end
|
||||
return nil
|
||||
end,
|
||||
saveSetting = function(_, key, value)
|
||||
if key == "download_dir" then download_dir = value end
|
||||
end,
|
||||
}
|
||||
|
||||
local detail = {
|
||||
id = 1,
|
||||
title = "Annihilation",
|
||||
fallbackFilename = "Annihilation - Jeff VanderMeer",
|
||||
}
|
||||
local file = {
|
||||
id = 10,
|
||||
format = "epub",
|
||||
devicePath = "Series/Southern Reach/1.00 - Annihilation.epub",
|
||||
}
|
||||
|
||||
local folder, filename = Catalog:getLocalDownloadPreview(detail.fallbackFilename, file.format, file.devicePath, false)
|
||||
assertEqual(folder, "/mnt/onboard/library/Series/Southern Reach", "configured preview folder")
|
||||
assertEqual(filename, "1.00 - Annihilation", "configured preview filename")
|
||||
assertEqual(#made_paths, 0, "preview does not create directories")
|
||||
|
||||
local resolved = Catalog:getLocalDownloadPath(detail.fallbackFilename, file.format, file.devicePath)
|
||||
assertEqual(resolved, "/mnt/onboard/library/Series/Southern Reach/1.00 - Annihilation.epub", "configured destination")
|
||||
assertEqual(made_paths[1], "/mnt/onboard/library/Series/Southern Reach", "configured parent created")
|
||||
|
||||
made_paths = {}
|
||||
resolved = Catalog:getLocalDownloadPath("Preferred/Name", "EPUB", file.devicePath, true)
|
||||
assertEqual(resolved, "/mnt/onboard/library/Series/Southern Reach/Preferred_Name.epub", "filename override preserves configured folder")
|
||||
assertEqual(made_paths[1], "/mnt/onboard/library/Series/Southern Reach", "override parent created")
|
||||
|
||||
made_paths = {}
|
||||
resolved = Catalog:getLocalDownloadPath("Fallback", "epub", "../outside.epub")
|
||||
assertEqual(resolved, "/mnt/onboard/library/Fallback.epub", "unsafe device path falls back inside download folder")
|
||||
assertEqual(#made_paths, 0, "unsafe device path does not create directories")
|
||||
|
||||
resolved = Catalog:getLocalDownloadPath("Fallback", "epub", "./Series//Book.epub")
|
||||
assertEqual(resolved, "/mnt/onboard/library/Series/Book.epub", "device path normalization remains unchanged")
|
||||
|
||||
download_dir = "/"
|
||||
folder, filename = Catalog:getLocalDownloadPreview("Fallback", "epub", "Book.epub", false)
|
||||
assertEqual(folder, "/", "root preview folder")
|
||||
assertEqual(filename, "Book", "root preview filename")
|
||||
assertEqual(Catalog:getLocalDownloadPath("Fallback", "epub", "Book.epub"), "/Book.epub", "root destination")
|
||||
download_dir = "/mnt/onboard/library"
|
||||
made_paths = {}
|
||||
|
||||
local downloaded_path
|
||||
Catalog.checkDownloadFile = function(_, path)
|
||||
downloaded_path = path
|
||||
end
|
||||
|
||||
Catalog:showDownloadDialog(detail, file)
|
||||
local dialog = shown_widget
|
||||
assertContains(dialog.title, "Download folder:\n/mnt/onboard/library/Series/Southern Reach", "dialog shows resolved folder")
|
||||
assertContains(dialog.title, "Download filename:\n1.00 - Annihilation", "dialog shows resolved filename")
|
||||
assertEqual(#made_paths, 0, "opening dialog does not create directories")
|
||||
dialog.buttons[1][1].callback()
|
||||
assertEqual(downloaded_path, "/mnt/onboard/library/Series/Southern Reach/1.00 - Annihilation.epub", "dialog downloads previewed destination")
|
||||
|
||||
downloaded_path = nil
|
||||
Catalog:showDownloadDialog(detail, file)
|
||||
dialog = shown_widget
|
||||
dialog.buttons[3][2].callback()
|
||||
local input_dialog = shown_widget
|
||||
assertEqual(input_dialog.input, "1.00 - Annihilation", "filename editor starts with resolved filename")
|
||||
typed_filename = " Preferred/Name "
|
||||
input_dialog.buttons[1][2].callback()
|
||||
assertContains(dialog.title, "Download folder:\n/mnt/onboard/library/Series/Southern Reach", "override keeps resolved folder in dialog")
|
||||
assertContains(dialog.title, "Download filename:\nPreferred_Name", "dialog previews sanitized override")
|
||||
dialog.buttons[1][1].callback()
|
||||
assertEqual(downloaded_path, "/mnt/onboard/library/Series/Southern Reach/Preferred_Name.epub", "filename editor changes downloaded filename")
|
||||
typed_filename = nil
|
||||
|
||||
file.devicePath = nil
|
||||
Catalog:showDownloadDialog(detail, file)
|
||||
dialog = shown_widget
|
||||
assertContains(dialog.title, "Download folder:\n/mnt/onboard/library", "legacy dialog keeps base folder")
|
||||
assertContains(dialog.title, "Download filename:\nAnnihilation - Jeff VanderMeer", "legacy dialog keeps fallback filename")
|
||||
dialog.buttons[1][1].callback()
|
||||
assertEqual(downloaded_path, "/mnt/onboard/library/Annihilation - Jeff VanderMeer.epub", "legacy download remains unchanged")
|
||||
|
||||
file.devicePath = "Series/Southern Reach/1.00 - Annihilation.epub"
|
||||
package.loaded["ui/downloadmgr"] = {
|
||||
new = function(_, opts)
|
||||
function opts:chooseDir()
|
||||
self.onConfirm("/mnt/onboard/books")
|
||||
end
|
||||
return opts
|
||||
end,
|
||||
}
|
||||
Catalog:showDownloadDialog(detail, file)
|
||||
dialog = shown_widget
|
||||
dialog.buttons[3][2].callback()
|
||||
input_dialog = shown_widget
|
||||
typed_filename = "Custom Name"
|
||||
input_dialog.buttons[1][2].callback()
|
||||
dialog.buttons[3][1].callback()
|
||||
assertEqual(#scheduled, 1, "folder change queues refreshed dialog")
|
||||
table.remove(scheduled, 1)()
|
||||
dialog = shown_widget
|
||||
assertContains(dialog.title, "Download folder:\n/mnt/onboard/books/Series/Southern Reach", "folder change refreshes resolved folder")
|
||||
assertContains(dialog.title, "Download filename:\nCustom Name", "folder change preserves filename override")
|
||||
dialog.buttons[1][1].callback()
|
||||
assertEqual(downloaded_path, "/mnt/onboard/books/Series/Southern Reach/Custom Name.epub", "combined folder and filename changes match preview")
|
||||
|
||||
print("bookorbit_catalog_download_test.lua: ok")
|
||||
Reference in New Issue
Block a user