mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
joins calibre metadata search and calibre wireless connections into a single plugin search metadata changes: - search directly into calibre metadata files. - search can be performed on more than one library (configurable from a menu) - device scans now find all calibre libraries under a given root - search options can be configured from a menu. (case sensitive, find by title, author and path) - removed legacy global variables. - *option* to search from the reader - *option* to generate a cache of books for faster searches. calibre wireless connection changes: - keep track of books in a library (includes prunning books from calibre metadata if the file was deleted locally) - remove files on device from calibre - support password protected connections - FM integration: if we're in the inbox dir it will be updated each time a book is added or deleted. - disconnect when requested by calibre, available on newer calibre versions (+4.17) - remove unused opcodes. - better report of client name, version and device id - free disk space checks for all calibre versions - bump supported extensions to match what KOReader can handle. Users can override this with their own list of extensions (or from calibre, by configuring the wireless device).
1062 lines
40 KiB
Lua
1062 lines
40 KiB
Lua
local BD = require("ui/bidi")
|
||
local Blitbuffer = require("ffi/blitbuffer")
|
||
local Button = require("ui/widget/button")
|
||
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
|
||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||
local ConfirmBox = require("ui/widget/confirmbox")
|
||
local Device = require("device")
|
||
local DocSettings = require("docsettings")
|
||
local DocumentRegistry = require("document/documentregistry")
|
||
local Event = require("ui/event")
|
||
local FileChooser = require("ui/widget/filechooser")
|
||
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
|
||
local FileManagerCollection = require("apps/filemanager/filemanagercollection")
|
||
local FileManagerConverter = require("apps/filemanager/filemanagerconverter")
|
||
local FileManagerFileSearcher = require("apps/filemanager/filemanagerfilesearcher")
|
||
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
|
||
local FileManagerMenu = require("apps/filemanager/filemanagermenu")
|
||
local FileManagerShortcuts = require("apps/filemanager/filemanagershortcuts")
|
||
local Font = require("ui/font")
|
||
local FrameContainer = require("ui/widget/container/framecontainer")
|
||
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
||
local IconButton = require("ui/widget/iconbutton")
|
||
local InfoMessage = require("ui/widget/infomessage")
|
||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||
local InputDialog = require("ui/widget/inputdialog")
|
||
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
||
local PluginLoader = require("pluginloader")
|
||
local ReadCollection = require("readcollection")
|
||
local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
|
||
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
|
||
local ReaderGesture = require("apps/reader/modules/readergesture")
|
||
local ReaderUI = require("apps/reader/readerui")
|
||
local ReaderWikipedia = require("apps/reader/modules/readerwikipedia")
|
||
local Screenshoter = require("ui/widget/screenshoter")
|
||
local Size = require("ui/size")
|
||
local TextWidget = require("ui/widget/textwidget")
|
||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||
local VerticalSpan = require("ui/widget/verticalspan")
|
||
local UIManager = require("ui/uimanager")
|
||
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
||
local lfs = require("libs/libkoreader-lfs")
|
||
local logger = require("logger")
|
||
local BaseUtil = require("ffi/util")
|
||
local util = require("util")
|
||
local _ = require("gettext")
|
||
local C_ = _.pgettext
|
||
local Screen = Device.screen
|
||
local T = require("ffi/util").template
|
||
|
||
local function restoreScreenMode()
|
||
--- @todo: Not Yet Implemented. Layout is currently broken in Landscape.
|
||
local screen_mode = G_reader_settings:readSetting("fm_screen_mode") or "portrait"
|
||
--- @note: Basically, if we were already in Portrait/Inverted Portrait, don't mess with it,
|
||
-- as the FM supports it.
|
||
-- See setScreenMode in base's ffi/framebuffer.lua for the gory details.
|
||
-- See also ReaderView:onSetScreenMode in apps/reader/modules/readerview.lua for a similar logic,
|
||
-- if we ever need to add Landscape to the mix.
|
||
-- c.f., https://github.com/koreader/koreader/issues/5772#issuecomment-577242365
|
||
if Screen:getScreenMode() ~= screen_mode then
|
||
Screen:setScreenMode(screen_mode)
|
||
end
|
||
end
|
||
|
||
local FileManager = InputContainer:extend{
|
||
title = _("KOReader"),
|
||
root_path = lfs.currentdir(),
|
||
onExit = function() end,
|
||
|
||
mv_bin = Device:isAndroid() and "/system/bin/mv" or "/bin/mv",
|
||
cp_bin = Device:isAndroid() and "/system/bin/cp" or "/bin/cp",
|
||
mkdir_bin = Device:isAndroid() and "/system/bin/mkdir" or "/bin/mkdir",
|
||
}
|
||
|
||
function FileManager:init()
|
||
if Device:isTouchDevice() then
|
||
self:registerTouchZones({
|
||
{
|
||
id = "filemanager_swipe",
|
||
ges = "swipe",
|
||
screen_zone = {
|
||
ratio_x = 0, ratio_y = 0,
|
||
ratio_w = Screen:getWidth(), ratio_h = Screen:getHeight(),
|
||
},
|
||
handler = function(ges)
|
||
self:onSwipeFM(ges)
|
||
end,
|
||
},
|
||
})
|
||
end
|
||
self.show_parent = self.show_parent or self
|
||
local icon_size = Screen:scaleBySize(35)
|
||
local home_button = IconButton:new{
|
||
icon_file = "resources/icons/appbar.home.png",
|
||
scale_for_dpi = false,
|
||
width = icon_size,
|
||
height = icon_size,
|
||
padding = Size.padding.default,
|
||
padding_left = Size.padding.large,
|
||
padding_right = Size.padding.large,
|
||
padding_bottom = 0,
|
||
callback = function() self:goHome() end,
|
||
hold_callback = function() self:setHome() end,
|
||
}
|
||
|
||
local plus_button = IconButton:new{
|
||
icon_file = "resources/icons/appbar.plus.png",
|
||
scale_for_dpi = false,
|
||
width = icon_size,
|
||
height = icon_size,
|
||
padding = Size.padding.default,
|
||
padding_left = Size.padding.large,
|
||
padding_right = Size.padding.large,
|
||
padding_bottom = 0,
|
||
callback = nil, -- top right corner callback handled by gesture manager
|
||
hold_callback = nil, -- top right corner hold_callback handled by gesture manager
|
||
}
|
||
|
||
self.path_text = TextWidget:new{
|
||
face = Font:getFace("xx_smallinfofont"),
|
||
text = BD.directory(filemanagerutil.abbreviate(self.root_path)),
|
||
max_width = Screen:getWidth() - 2*Size.padding.small,
|
||
truncate_left = true,
|
||
}
|
||
|
||
self.banner = FrameContainer:new{
|
||
padding = 0,
|
||
bordersize = 0,
|
||
VerticalGroup:new {
|
||
CenterContainer:new {
|
||
dimen = { w = Screen:getWidth(), h = nil },
|
||
HorizontalGroup:new {
|
||
home_button,
|
||
VerticalGroup:new {
|
||
Button:new {
|
||
readonly = true,
|
||
bordersize = 0,
|
||
padding = 0,
|
||
text_font_bold = false,
|
||
text_font_face = "smalltfont",
|
||
text_font_size = 24,
|
||
text = self.title,
|
||
width = Screen:getWidth() - 2 * icon_size - 4 * Size.padding.large,
|
||
},
|
||
},
|
||
plus_button,
|
||
}
|
||
},
|
||
CenterContainer:new{
|
||
dimen = { w = Screen:getWidth(), h = nil },
|
||
self.path_text,
|
||
},
|
||
VerticalSpan:new{ width = Screen:scaleBySize(5) },
|
||
}
|
||
}
|
||
|
||
local g_show_hidden = G_reader_settings:readSetting("show_hidden")
|
||
local show_hidden = g_show_hidden == nil and DSHOWHIDDENFILES or g_show_hidden
|
||
local show_unsupported = G_reader_settings:readSetting("show_unsupported")
|
||
local file_chooser = FileChooser:new{
|
||
-- remember to adjust the height when new item is added to the group
|
||
path = self.root_path,
|
||
focused_path = self.focused_file,
|
||
collate = G_reader_settings:readSetting("collate") or "strcoll",
|
||
reverse_collate = G_reader_settings:isTrue("reverse_collate"),
|
||
show_parent = self.show_parent,
|
||
show_hidden = show_hidden,
|
||
width = Screen:getWidth(),
|
||
height = Screen:getHeight() - self.banner:getSize().h,
|
||
is_popout = false,
|
||
is_borderless = true,
|
||
has_close_button = true,
|
||
perpage = G_reader_settings:readSetting("items_per_page"),
|
||
show_unsupported = show_unsupported,
|
||
file_filter = function(filename)
|
||
if DocumentRegistry:hasProvider(filename) then
|
||
return true
|
||
end
|
||
end,
|
||
close_callback = function() return self:onClose() end,
|
||
-- allow left bottom tap gesture, otherwise it is eaten by hidden return button
|
||
return_arrow_propagation = true,
|
||
-- allow Menu widget to delegate handling of some gestures to GestureManager
|
||
is_file_manager = true,
|
||
}
|
||
self.file_chooser = file_chooser
|
||
self.focused_file = nil -- use it only once
|
||
|
||
function file_chooser:onPathChanged(path) -- luacheck: ignore
|
||
FileManager.instance.path_text:setText(BD.directory(filemanagerutil.abbreviate(path)))
|
||
UIManager:setDirty(FileManager.instance, function()
|
||
return "ui", FileManager.instance.path_text.dimen, FileManager.instance.dithered
|
||
end)
|
||
return true
|
||
end
|
||
|
||
function file_chooser:onFileSelect(file) -- luacheck: ignore
|
||
FileManager.instance:onClose()
|
||
ReaderUI:showReader(file)
|
||
return true
|
||
end
|
||
|
||
local copyFile = function(file) self:copyFile(file) end
|
||
local pasteHere = function(file) self:pasteHere(file) end
|
||
local cutFile = function(file) self:cutFile(file) end
|
||
local deleteFile = function(file) self:deleteFile(file) end
|
||
local renameFile = function(file) self:renameFile(file) end
|
||
local setHome = function(path) self:setHome(path) end
|
||
local fileManager = self
|
||
|
||
function file_chooser:onFileHold(file) -- luacheck: ignore
|
||
local buttons = {
|
||
{
|
||
{
|
||
text = C_("File", "Copy"),
|
||
callback = function()
|
||
copyFile(file)
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = C_("File", "Paste"),
|
||
enabled = fileManager.clipboard and true or false,
|
||
callback = function()
|
||
pasteHere(file)
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Purge .sdr"),
|
||
enabled = DocSettings:hasSidecarFile(BaseUtil.realpath(file)),
|
||
callback = function()
|
||
UIManager:show(ConfirmBox:new{
|
||
text = T(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(self.file_dialog.title)),
|
||
ok_text = _("Purge"),
|
||
ok_callback = function()
|
||
filemanagerutil.purgeSettings(file)
|
||
require("readhistory"):fileSettingsPurged(file)
|
||
self:refreshPath()
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
})
|
||
end,
|
||
},
|
||
},
|
||
{
|
||
{
|
||
text = _("Cut"),
|
||
callback = function()
|
||
cutFile(file)
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Delete"),
|
||
callback = function()
|
||
UIManager:show(ConfirmBox:new{
|
||
text = _("Are you sure that you want to delete this file?\n") .. BD.filepath(file) .. ("\n") .. _("If you delete a file, it is permanently lost."),
|
||
ok_text = _("Delete"),
|
||
ok_callback = function()
|
||
deleteFile(file)
|
||
require("readhistory"):fileDeleted(file)
|
||
self:refreshPath()
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
})
|
||
end,
|
||
},
|
||
{
|
||
text = _("Rename"),
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
fileManager.rename_dialog = InputDialog:new{
|
||
title = _("Rename file"),
|
||
input = BaseUtil.basename(file),
|
||
buttons = {{
|
||
{
|
||
text = _("Cancel"),
|
||
enabled = true,
|
||
callback = function()
|
||
UIManager:close(fileManager.rename_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Rename"),
|
||
enabled = true,
|
||
callback = function()
|
||
renameFile(file)
|
||
self:refreshPath()
|
||
UIManager:close(fileManager.rename_dialog)
|
||
end,
|
||
},
|
||
}},
|
||
}
|
||
UIManager:show(fileManager.rename_dialog)
|
||
fileManager.rename_dialog:onShowKeyboard()
|
||
end,
|
||
}
|
||
},
|
||
-- a little hack to get visual functionality grouping
|
||
{
|
||
},
|
||
}
|
||
|
||
if not Device:isAndroid() and lfs.attributes(file, "mode") == "file" and util.isAllowedScript(file) then
|
||
-- NOTE: We populate the empty separator, in order not to mess with the button reordering code in CoverMenu
|
||
table.insert(buttons[3],
|
||
{
|
||
-- @translators This is the script's programming language (e.g., shell or python)
|
||
text = T(_("Execute %1 script"), util.getScriptType(file)),
|
||
enabled = true,
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
local script_is_running_msg = InfoMessage:new{
|
||
-- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename
|
||
text = T(_("Running %1 script %2…"), util.getScriptType(file), BD.filename(BaseUtil.basename(file))),
|
||
}
|
||
UIManager:show(script_is_running_msg)
|
||
UIManager:scheduleIn(0.5, function()
|
||
local rv = os.execute(BaseUtil.realpath(file))
|
||
UIManager:close(script_is_running_msg)
|
||
if rv == 0 then
|
||
UIManager:show(InfoMessage:new{
|
||
text = _("The script exited successfully."),
|
||
})
|
||
else
|
||
--- @note: Lua 5.1 returns the raw return value from the os's system call. Counteract this madness.
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)),
|
||
icon_file = "resources/info-warn.png",
|
||
})
|
||
end
|
||
end)
|
||
end,
|
||
}
|
||
)
|
||
end
|
||
|
||
if lfs.attributes(file, "mode") == "file" then
|
||
table.insert(buttons, {
|
||
{
|
||
text = _("Open with…"),
|
||
enabled = DocumentRegistry:getProviders(file) == nil or #(DocumentRegistry:getProviders(file)) > 1,
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
self:showSetProviderButtons(file, FileManager.instance, ReaderUI)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Book information"),
|
||
enabled = FileManagerBookInfo:isSupported(file),
|
||
callback = function()
|
||
FileManagerBookInfo:show(file)
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
}
|
||
})
|
||
table.insert(buttons, {
|
||
{
|
||
text_func = function()
|
||
if ReadCollection:checkItemExist(file) then
|
||
return _("Remove from favorites")
|
||
else
|
||
return _("Add to favorites")
|
||
end
|
||
end,
|
||
enabled = DocumentRegistry:getProviders(file) ~= nil,
|
||
callback = function()
|
||
if ReadCollection:checkItemExist(file) then
|
||
ReadCollection:removeItem(file)
|
||
else
|
||
ReadCollection:addItem(file)
|
||
end
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
})
|
||
if FileManagerConverter:isSupported(file) then
|
||
table.insert(buttons, {
|
||
{
|
||
text = _("Convert"),
|
||
enabled = true,
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
FileManagerConverter:showConvertButtons(file, self)
|
||
end,
|
||
}
|
||
})
|
||
end
|
||
end
|
||
if lfs.attributes(file, "mode") == "directory" then
|
||
local realpath = BaseUtil.realpath(file)
|
||
table.insert(buttons, {
|
||
{
|
||
text = _("Set as HOME directory"),
|
||
callback = function()
|
||
setHome(realpath)
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
})
|
||
end
|
||
|
||
local title
|
||
if lfs.attributes(file, "mode") == "directory" then
|
||
title = BD.directory(file:match("([^/]+)$"))
|
||
else
|
||
title = BD.filename(file:match("([^/]+)$"))
|
||
end
|
||
|
||
self.file_dialog = ButtonDialogTitle:new{
|
||
title = title,
|
||
title_align = "center",
|
||
buttons = buttons,
|
||
}
|
||
UIManager:show(self.file_dialog)
|
||
return true
|
||
end
|
||
|
||
self.layout = VerticalGroup:new{
|
||
self.banner,
|
||
file_chooser,
|
||
}
|
||
|
||
local fm_ui = FrameContainer:new{
|
||
padding = 0,
|
||
bordersize = 0,
|
||
background = Blitbuffer.COLOR_WHITE,
|
||
self.layout,
|
||
}
|
||
|
||
self[1] = fm_ui
|
||
|
||
self.menu = FileManagerMenu:new{
|
||
ui = self
|
||
}
|
||
self.active_widgets = { Screenshoter:new{ prefix = 'FileManager' } }
|
||
table.insert(self, self.menu)
|
||
table.insert(self, FileManagerHistory:new{
|
||
ui = self,
|
||
})
|
||
table.insert(self, FileManagerCollection:new{
|
||
ui = self,
|
||
})
|
||
table.insert(self, FileManagerFileSearcher:new{ ui = self })
|
||
table.insert(self, FileManagerShortcuts:new{ ui = self })
|
||
table.insert(self, ReaderDictionary:new{ ui = self })
|
||
table.insert(self, ReaderWikipedia:new{ ui = self })
|
||
table.insert(self, ReaderDeviceStatus:new{ ui = self })
|
||
|
||
-- koreader plugins
|
||
for _,plugin_module in ipairs(PluginLoader:loadPlugins()) do
|
||
if not plugin_module.is_doc_only then
|
||
local ok, plugin_or_err = PluginLoader:createPluginInstance(
|
||
plugin_module, { ui = self, })
|
||
-- Keep references to the modules which do not register into menu.
|
||
if ok then
|
||
local name = plugin_module.name
|
||
if name then self[name] = plugin_or_err end
|
||
table.insert(self, plugin_or_err)
|
||
logger.info("FM loaded plugin", name,
|
||
"at", plugin_module.path)
|
||
end
|
||
end
|
||
end
|
||
|
||
if Device:isTouchDevice() then
|
||
table.insert(self, ReaderGesture:new{ ui = self })
|
||
end
|
||
|
||
if Device:hasKeys() then
|
||
self.key_events.Home = { {"Home"}, doc = "go home" }
|
||
--Override the menu.lua way of handling the back key
|
||
self.file_chooser.key_events.Back = { {"Back"}, doc = "go back" }
|
||
end
|
||
|
||
self:handleEvent(Event:new("SetDimensions", self.dimen))
|
||
end
|
||
|
||
function FileChooser:onBack()
|
||
local back_to_exit = G_reader_settings:readSetting("back_to_exit") or "prompt"
|
||
local back_in_filemanager = G_reader_settings:readSetting("back_in_filemanager") or "default"
|
||
if back_in_filemanager == "default" then
|
||
if back_to_exit == "always" then
|
||
return self:onClose()
|
||
elseif back_to_exit == "disable" then
|
||
return true
|
||
elseif back_to_exit == "prompt" then
|
||
UIManager:show(ConfirmBox:new{
|
||
text = _("Exit KOReader?"),
|
||
ok_text = _("Exit"),
|
||
ok_callback = function()
|
||
self:onClose()
|
||
end
|
||
})
|
||
|
||
return true
|
||
end
|
||
elseif back_in_filemanager == "parent_folder" then
|
||
self:changeToPath(string.format("%s/..", self.path))
|
||
return true
|
||
end
|
||
end
|
||
|
||
function FileManager:onShowPlusMenu()
|
||
self:tapPlus()
|
||
return true
|
||
end
|
||
|
||
function FileManager:onSwipeFM(ges)
|
||
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
|
||
if direction == "west" then
|
||
self.file_chooser:onNextPage()
|
||
elseif direction == "east" then
|
||
self.file_chooser:onPrevPage()
|
||
end
|
||
return true
|
||
end
|
||
|
||
function FileManager:tapPlus()
|
||
local buttons = {
|
||
{
|
||
{
|
||
text = _("New folder"),
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
self.input_dialog = InputDialog:new{
|
||
title = _("Create new folder"),
|
||
input_type = "text",
|
||
buttons = {
|
||
{
|
||
{
|
||
text = _("Cancel"),
|
||
callback = function()
|
||
self:closeInputDialog()
|
||
end,
|
||
},
|
||
{
|
||
text = _("Create"),
|
||
callback = function()
|
||
self:closeInputDialog()
|
||
local new_folder = self.input_dialog:getInputText()
|
||
if new_folder and new_folder ~= "" then
|
||
self:createFolder(self.file_chooser.path, new_folder)
|
||
end
|
||
end,
|
||
},
|
||
}
|
||
},
|
||
}
|
||
UIManager:show(self.input_dialog)
|
||
self.input_dialog:onShowKeyboard()
|
||
end,
|
||
},
|
||
},
|
||
{
|
||
{
|
||
text = _("Paste"),
|
||
enabled = self.clipboard and true or false,
|
||
callback = function()
|
||
self:pasteHere(self.file_chooser.path)
|
||
self:onRefresh()
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
},
|
||
{
|
||
{
|
||
text = _("Set as HOME directory"),
|
||
callback = function()
|
||
self:setHome(self.file_chooser.path)
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
},
|
||
{
|
||
{
|
||
text = _("Go to HOME directory"),
|
||
callback = function()
|
||
self:goHome()
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
},
|
||
{
|
||
{
|
||
text = _("Open random document"),
|
||
callback = function()
|
||
self:openRandomFile(self.file_chooser.path)
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
},
|
||
{
|
||
{
|
||
text = _("Folder shortcuts"),
|
||
callback = function()
|
||
self:handleEvent(Event:new("ShowFolderShortcutsDialog"))
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
}
|
||
}
|
||
|
||
if Device:canImportFiles() then
|
||
table.insert(buttons, 3, {
|
||
{
|
||
text = _("Import files here"),
|
||
enabled = Device.isValidPath(self.file_chooser.path),
|
||
callback = function()
|
||
local current_dir = self.file_chooser.path
|
||
UIManager:close(self.file_dialog)
|
||
Device.importFile(current_dir)
|
||
end,
|
||
},
|
||
})
|
||
end
|
||
|
||
self.file_dialog = ButtonDialogTitle:new{
|
||
title = BD.dirpath(filemanagerutil.abbreviate(self.file_chooser.path)),
|
||
title_align = "center",
|
||
buttons = buttons,
|
||
}
|
||
UIManager:show(self.file_dialog)
|
||
end
|
||
|
||
function FileManager:reinit(path, focused_file)
|
||
self.dimen = Screen:getSize()
|
||
-- backup the root path and path items
|
||
self.root_path = path or self.file_chooser.path
|
||
local path_items_backup = {}
|
||
for k, v in pairs(self.file_chooser.path_items) do
|
||
path_items_backup[k] = v
|
||
end
|
||
-- reinit filemanager
|
||
self.focused_file = focused_file
|
||
self:init()
|
||
self.file_chooser.path_items = path_items_backup
|
||
-- self:init() has already done file_chooser:refreshPath(), so this one
|
||
-- looks like not necessary (cheap with classic mode, less cheap with
|
||
-- CoverBrowser plugin's cover image renderings)
|
||
-- self:onRefresh()
|
||
end
|
||
|
||
function FileManager:getCurrentDir()
|
||
if self.instance then
|
||
return self.instance.file_chooser.path
|
||
end
|
||
end
|
||
|
||
function FileManager:toggleHiddenFiles()
|
||
self.file_chooser:toggleHiddenFiles()
|
||
G_reader_settings:saveSetting("show_hidden", self.file_chooser.show_hidden)
|
||
end
|
||
|
||
function FileManager:toggleUnsupportedFiles()
|
||
self.file_chooser:toggleUnsupportedFiles()
|
||
G_reader_settings:saveSetting("show_unsupported", self.file_chooser.show_unsupported)
|
||
end
|
||
|
||
function FileManager:setCollate(collate)
|
||
self.file_chooser:setCollate(collate)
|
||
G_reader_settings:saveSetting("collate", self.file_chooser.collate)
|
||
end
|
||
|
||
function FileManager:toggleReverseCollate()
|
||
self.file_chooser:toggleReverseCollate()
|
||
G_reader_settings:saveSetting("reverse_collate", self.file_chooser.reverse_collate)
|
||
end
|
||
|
||
function FileManager:onClose()
|
||
logger.dbg("close filemanager")
|
||
self:handleEvent(Event:new("SaveSettings"))
|
||
G_reader_settings:flush()
|
||
UIManager:close(self)
|
||
if self.onExit then
|
||
self:onExit()
|
||
end
|
||
return true
|
||
end
|
||
|
||
function FileManager:onRefresh()
|
||
self.file_chooser:refreshPath()
|
||
return true
|
||
end
|
||
|
||
function FileManager:goHome()
|
||
local home_dir = G_reader_settings:readSetting("home_dir")
|
||
if not home_dir or lfs.attributes(home_dir, "mode") ~= "directory" then
|
||
-- Try some sane defaults, depending on platform
|
||
home_dir = Device.home_dir
|
||
end
|
||
if home_dir then
|
||
-- Jump to the first page if we're already home
|
||
if self.file_chooser.path and home_dir == self.file_chooser.path then
|
||
self.file_chooser:onGotoPage(1)
|
||
else
|
||
self.file_chooser:changeToPath(home_dir)
|
||
end
|
||
else
|
||
self:setHome()
|
||
end
|
||
return true
|
||
end
|
||
|
||
function FileManager:setHome(path)
|
||
path = path or self.file_chooser.path
|
||
UIManager:show(ConfirmBox:new{
|
||
text = T(_("Set '%1' as HOME directory?"), BD.dirpath(path)),
|
||
ok_text = _("Set as HOME"),
|
||
ok_callback = function()
|
||
G_reader_settings:saveSetting("home_dir", path)
|
||
end,
|
||
})
|
||
return true
|
||
end
|
||
|
||
function FileManager:openRandomFile(dir)
|
||
local random_file = DocumentRegistry:getRandomFile(dir, false)
|
||
if random_file then
|
||
UIManager:show(MultiConfirmBox:new {
|
||
text = T(_("Do you want to open %1?"), BD.filename(BaseUtil.basename(random_file))),
|
||
choice1_text = _("Open"),
|
||
choice1_callback = function()
|
||
FileManager.instance:onClose()
|
||
ReaderUI:showReader(random_file)
|
||
|
||
end,
|
||
-- @translators Another file. This is a button on the open random file dialog. It presents a file with the choices Open/Another.
|
||
choice2_text = _("Another"),
|
||
choice2_callback = function()
|
||
self:openRandomFile(dir)
|
||
end,
|
||
})
|
||
UIManager:close(self.file_dialog)
|
||
else
|
||
UIManager:show(InfoMessage:new {
|
||
text = _("File not found"),
|
||
})
|
||
end
|
||
end
|
||
|
||
function FileManager:copyFile(file)
|
||
self.cutfile = false
|
||
self.clipboard = file
|
||
end
|
||
|
||
function FileManager:cutFile(file)
|
||
self.cutfile = true
|
||
self.clipboard = file
|
||
end
|
||
|
||
function FileManager:pasteHere(file)
|
||
if self.clipboard then
|
||
file = BaseUtil.realpath(file)
|
||
local orig = BaseUtil.realpath(self.clipboard)
|
||
local dest = lfs.attributes(file, "mode") == "directory" and
|
||
file or file:match("(.*/)")
|
||
|
||
local function infoCopyFile()
|
||
-- if we copy a file, also copy its sidecar directory
|
||
if DocSettings:hasSidecarFile(orig) then
|
||
BaseUtil.execute(self.cp_bin, "-r", DocSettings:getSidecarDir(orig), dest)
|
||
end
|
||
if BaseUtil.execute(self.cp_bin, "-r", orig, dest) == 0 then
|
||
UIManager:show(InfoMessage:new {
|
||
text = T(_("Copied to: %1"), BD.dirpath(dest)),
|
||
timeout = 2,
|
||
})
|
||
else
|
||
UIManager:show(InfoMessage:new {
|
||
text = T(_("An error occurred while trying to copy %1"), BD.filepath(orig)),
|
||
timeout = 2,
|
||
})
|
||
end
|
||
end
|
||
|
||
local function infoMoveFile()
|
||
-- if we move a file, also move its sidecar directory
|
||
if DocSettings:hasSidecarFile(orig) then
|
||
self:moveFile(DocSettings:getSidecarDir(orig), dest) -- dest is always a directory
|
||
end
|
||
if self:moveFile(orig, dest) then
|
||
-- Update history and collections.
|
||
local dest_file = string.format("%s/%s", dest, BaseUtil.basename(orig))
|
||
require("readhistory"):updateItemByPath(orig, dest_file) -- (will update "lastfile" if needed)
|
||
ReadCollection:updateItemByPath(orig, dest_file)
|
||
UIManager:show(InfoMessage:new {
|
||
text = T(_("Moved to: %1"), BD.dirpath(dest)),
|
||
timeout = 2,
|
||
})
|
||
else
|
||
UIManager:show(InfoMessage:new {
|
||
text = T(_("An error occurred while trying to move %1"), BD.filepath(orig)),
|
||
timeout = 2,
|
||
})
|
||
end
|
||
end
|
||
|
||
local info_file
|
||
if self.cutfile then
|
||
info_file = infoMoveFile
|
||
else
|
||
info_file = infoCopyFile
|
||
end
|
||
local basename = BaseUtil.basename(self.clipboard)
|
||
local mode = lfs.attributes(string.format("%s/%s", dest, basename), "mode")
|
||
if mode == "file" or mode == "directory" then
|
||
local text
|
||
if mode == "file" then
|
||
text = T(_("The file %1 already exists. Do you want to overwrite it?"), BD.filename(basename))
|
||
else
|
||
text = T(_("The directory %1 already exists. Do you want to overwrite it?"), BD.directory(basename))
|
||
end
|
||
|
||
UIManager:show(ConfirmBox:new {
|
||
text = text,
|
||
ok_text = _("Overwrite"),
|
||
ok_callback = function()
|
||
info_file()
|
||
self:onRefresh()
|
||
self.clipboard = nil
|
||
end,
|
||
})
|
||
else
|
||
info_file()
|
||
self:onRefresh()
|
||
self.clipboard = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
function FileManager:createFolder(curr_folder, new_folder)
|
||
local folder = string.format("%s/%s", curr_folder, new_folder)
|
||
local code = BaseUtil.execute(self.mkdir_bin, folder)
|
||
local text
|
||
if code == 0 then
|
||
self:onRefresh()
|
||
text = T(_("Folder created:\n%1"), BD.directory(new_folder))
|
||
else
|
||
text = _("The folder has not been created.")
|
||
end
|
||
UIManager:show(InfoMessage:new{
|
||
text = text,
|
||
timeout = 2,
|
||
})
|
||
end
|
||
|
||
function FileManager:deleteFile(file)
|
||
local ok, err, is_dir
|
||
local file_abs_path = BaseUtil.realpath(file)
|
||
if file_abs_path == nil then
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("File %1 not found"), BD.filepath(file)),
|
||
})
|
||
return
|
||
end
|
||
|
||
local is_doc = DocumentRegistry:hasProvider(file_abs_path)
|
||
if lfs.attributes(file_abs_path, "mode") == "file" then
|
||
ok, err = os.remove(file_abs_path)
|
||
else
|
||
ok, err = BaseUtil.purgeDir(file_abs_path)
|
||
is_dir = true
|
||
end
|
||
if ok and not err then
|
||
if is_doc then
|
||
local doc_settings = DocSettings:open(file)
|
||
-- remove cache if any
|
||
local cache_file_path = doc_settings:readSetting("cache_file_path")
|
||
if cache_file_path then
|
||
os.remove(cache_file_path)
|
||
end
|
||
doc_settings:purge()
|
||
end
|
||
ReadCollection:removeItemByPath(file, is_dir)
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("Deleted %1"), BD.filepath(file)),
|
||
timeout = 2,
|
||
})
|
||
else
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("An error occurred while trying to delete %1"), BD.filepath(file)),
|
||
})
|
||
end
|
||
end
|
||
|
||
function FileManager:renameFile(file)
|
||
if BaseUtil.basename(file) ~= self.rename_dialog:getInputText() then
|
||
local dest = BaseUtil.joinPath(BaseUtil.dirname(file), self.rename_dialog:getInputText())
|
||
if self:moveFile(file, dest) then
|
||
require("readhistory"):updateItemByPath(file, dest) -- (will update "lastfile" if needed)
|
||
ReadCollection:updateItemByPath(file, dest)
|
||
if lfs.attributes(dest, "mode") == "file" then
|
||
local doc = require("docsettings")
|
||
local move_history = true;
|
||
if lfs.attributes(doc:getHistoryPath(file), "mode") == "file" and
|
||
not self:moveFile(doc:getHistoryPath(file), doc:getHistoryPath(dest)) then
|
||
move_history = false
|
||
end
|
||
if lfs.attributes(doc:getSidecarDir(file), "mode") == "directory" and
|
||
not self:moveFile(doc:getSidecarDir(file), doc:getSidecarDir(dest)) then
|
||
move_history = false
|
||
end
|
||
if move_history then
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("Renamed from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
|
||
timeout = 2,
|
||
})
|
||
else
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(
|
||
_("Failed to move history data of %1 to %2.\nThe reading history may be lost."),
|
||
BD.filepath(file), BD.filepath(dest)),
|
||
})
|
||
end
|
||
end
|
||
else
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(
|
||
_("Failed to rename from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
|
||
})
|
||
end
|
||
end
|
||
end
|
||
|
||
function FileManager:getSortingMenuTable()
|
||
local fm = self
|
||
local collates = {
|
||
strcoll = {_("filename"), _("Sort by filename")},
|
||
strcoll_mixed = {_("name mixed"), _("Sort by name – mixed files and folders")},
|
||
access = {_("date read"), _("Sort by last read date")},
|
||
change = {_("date added"), _("Sort by date added")},
|
||
modification = {_("date modified"), _("Sort by date modified")},
|
||
size = {_("size"), _("Sort by size")},
|
||
type = {_("type"), _("Sort by type")},
|
||
percent_unopened_first = {_("percent – unopened first"), _("Sort by percent – unopened first")},
|
||
percent_unopened_last = {_("percent – unopened last"), _("Sort by percent – unopened last")},
|
||
}
|
||
local set_collate_table = function(collate)
|
||
return {
|
||
text = collates[collate][2],
|
||
checked_func = function()
|
||
return fm.file_chooser.collate == collate
|
||
end,
|
||
callback = function() fm:setCollate(collate) end,
|
||
}
|
||
end
|
||
local get_collate_percent = function()
|
||
local collate_type = G_reader_settings:readSetting("collate")
|
||
if collate_type == "percent_unopened_first" or collate_type == "percent_unopened_last" then
|
||
return collates[collate_type][2]
|
||
else
|
||
return _("Sort by percent")
|
||
end
|
||
end
|
||
return {
|
||
text_func = function()
|
||
return T(
|
||
_("Sort by: %1"),
|
||
collates[fm.file_chooser.collate][1]
|
||
)
|
||
end,
|
||
sub_item_table = {
|
||
set_collate_table("strcoll"),
|
||
set_collate_table("strcoll_mixed"),
|
||
set_collate_table("access"),
|
||
set_collate_table("change"),
|
||
set_collate_table("modification"),
|
||
set_collate_table("size"),
|
||
set_collate_table("type"),
|
||
{
|
||
text_func = get_collate_percent,
|
||
checked_func = function()
|
||
return fm.file_chooser.collate == "percent_unopened_first"
|
||
or fm.file_chooser.collate == "percent_unopened_last"
|
||
end,
|
||
sub_item_table = {
|
||
set_collate_table("percent_unopened_first"),
|
||
set_collate_table("percent_unopened_last"),
|
||
}
|
||
},
|
||
}
|
||
}
|
||
end
|
||
|
||
function FileManager:getStartWithMenuTable()
|
||
local start_with_setting = G_reader_settings:readSetting("start_with") or "filemanager"
|
||
local start_withs = {
|
||
filemanager = {_("file browser"), _("Start with file browser")},
|
||
history = {_("history"), _("Start with history")},
|
||
folder_shortcuts = {_("folder shortcuts"), _("Start with folder shortcuts")},
|
||
last = {_("last file"), _("Start with last file")},
|
||
}
|
||
local set_sw_table = function(start_with)
|
||
return {
|
||
text = start_withs[start_with][2],
|
||
checked_func = function()
|
||
return start_with_setting == start_with
|
||
end,
|
||
callback = function()
|
||
start_with_setting = start_with
|
||
G_reader_settings:saveSetting("start_with", start_with)
|
||
end,
|
||
}
|
||
end
|
||
return {
|
||
text_func = function()
|
||
return T(
|
||
_("Start with: %1"),
|
||
start_withs[start_with_setting][1]
|
||
)
|
||
end,
|
||
sub_item_table = {
|
||
set_sw_table("filemanager"),
|
||
set_sw_table("history"),
|
||
set_sw_table("folder_shortcuts"),
|
||
set_sw_table("last"),
|
||
}
|
||
}
|
||
end
|
||
|
||
function FileManager:showFiles(path, focused_file)
|
||
path = path or G_reader_settings:readSetting("lastdir") or filemanagerutil.getDefaultDir()
|
||
G_reader_settings:saveSetting("lastdir", path)
|
||
restoreScreenMode()
|
||
local file_manager = FileManager:new{
|
||
dimen = Screen:getSize(),
|
||
covers_fullscreen = true, -- hint for UIManager:_repaint()
|
||
root_path = path,
|
||
focused_file = focused_file,
|
||
onExit = function()
|
||
self.instance = nil
|
||
end
|
||
}
|
||
UIManager:show(file_manager)
|
||
self.instance = file_manager
|
||
end
|
||
|
||
--- A shortcut to execute mv.
|
||
-- @treturn boolean result of mv command
|
||
function FileManager:moveFile(from, to)
|
||
return BaseUtil.execute(self.mv_bin, from, to) == 0
|
||
end
|
||
|
||
--- A shortcut to execute cp.
|
||
-- @treturn boolean result of cp command
|
||
function FileManager:copyFileFromTo(from, to)
|
||
return BaseUtil.execute(self.cp_bin, from, to) == 0
|
||
end
|
||
|
||
--- A shortcut to execute cp recursively.
|
||
-- @treturn boolean result of cp command
|
||
function FileManager:copyRecursive(from, to)
|
||
return BaseUtil.execute(self.cp_bin, "-r", from, to ) == 0
|
||
end
|
||
|
||
function FileManager:onHome()
|
||
return self:goHome()
|
||
end
|
||
|
||
return FileManager
|