mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
When going from reader to filemanager, we are in the directory
containing the last_file. With this, we will also be on the page
showing this file.
When in filemanager and going up (".."), we will also be on the
page containing the directory we came from.
580 lines
20 KiB
Lua
580 lines
20 KiB
Lua
local Blitbuffer = require("ffi/blitbuffer")
|
|
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
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 FileManagerConverter = require("apps/filemanager/filemanagerconverter")
|
|
local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
|
|
local FileManagerMenu = require("apps/filemanager/filemanagermenu")
|
|
local Font = require("ui/font")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
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 OverlapGroup = require("ui/widget/overlapgroup")
|
|
local PluginLoader = require("pluginloader")
|
|
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
|
|
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 util = require("ffi/util")
|
|
local _ = require("gettext")
|
|
local Screen = Device.screen
|
|
|
|
local function restoreScreenMode()
|
|
local screen_mode = G_reader_settings:readSetting("fm_screen_mode")
|
|
if Screen:getScreenMode() ~= screen_mode then
|
|
Screen:setScreenMode(screen_mode or "portrait")
|
|
end
|
|
end
|
|
|
|
local FileManager = InputContainer:extend{
|
|
title = _("KOReader File Browser"),
|
|
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",
|
|
rm_bin = Device:isAndroid() and "/system/bin/rm" or "/bin/rm",
|
|
}
|
|
|
|
function FileManager:init()
|
|
self.show_parent = self.show_parent or self
|
|
|
|
local home_button = IconButton:new{
|
|
icon_file = "resources/icons/appbar.home.png",
|
|
width = Screen:scaleBySize(35),
|
|
height = Screen:scaleBySize(35),
|
|
padding = Size.padding.default,
|
|
padding_top = Size.padding.small,
|
|
padding_left = Size.padding.small,
|
|
callback = function() self:goHome() end,
|
|
hold_callback = function() self:setHome() end,
|
|
}
|
|
|
|
self.path_text = TextWidget:new{
|
|
face = Font:getFace("xx_smallinfofont"),
|
|
text = filemanagerutil.abbreviate(self.root_path),
|
|
}
|
|
|
|
self.banner = FrameContainer:new{
|
|
padding = 0,
|
|
bordersize = 0,
|
|
VerticalGroup:new{
|
|
OverlapGroup:new{
|
|
home_button,
|
|
VerticalGroup:new{
|
|
TextWidget:new{
|
|
face = Font:getFace("smalltfont"),
|
|
text = self.title,
|
|
},
|
|
CenterContainer:new{
|
|
dimen = { w = Screen:getWidth(), h = nil },
|
|
self.path_text,
|
|
},
|
|
},
|
|
},
|
|
VerticalSpan:new{ width = Screen:scaleBySize(10) },
|
|
},
|
|
}
|
|
|
|
local g_show_hidden = G_reader_settings:readSetting("show_hidden")
|
|
local show_hidden = g_show_hidden == nil and DSHOWHIDDENFILES or g_show_hidden
|
|
local file_chooser = FileChooser:new{
|
|
-- remeber to adjust the height when new item is added to the group
|
|
path = self.root_path,
|
|
collate = G_reader_settings:readSetting("collate") or "strcoll",
|
|
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,
|
|
file_filter = function(filename)
|
|
if DocumentRegistry:getProvider(filename) then
|
|
return true
|
|
end
|
|
end,
|
|
close_callback = function() return self:onClose() end,
|
|
}
|
|
self.file_chooser = file_chooser
|
|
|
|
function file_chooser:onPathChanged(path) -- luacheck: ignore
|
|
FileManager.instance.path_text:setText(filemanagerutil.abbreviate(path))
|
|
UIManager:setDirty(FileManager.instance, function()
|
|
return "ui", FileManager.instance.path_text.dimen
|
|
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 = _("Copy"),
|
|
callback = function()
|
|
copyFile(file)
|
|
UIManager:close(self.file_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Paste"),
|
|
enabled = fileManager.clipboard and true or false,
|
|
callback = function()
|
|
pasteHere(file)
|
|
self:refreshPath()
|
|
UIManager:close(self.file_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Purge .sdr"),
|
|
enabled = DocSettings:hasSidecarFile(util.realpath(file)),
|
|
callback = function()
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
UIManager:show(ConfirmBox:new{
|
|
text = util.template(_("Purge .sdr to reset settings for this document?\n\n%1"), self.file_dialog.title),
|
|
ok_text = _("Purge"),
|
|
ok_callback = function()
|
|
filemanagerutil.purgeSettings(file)
|
|
filemanagerutil.removeFileFromHistoryIfWanted(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()
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
UIManager:show(ConfirmBox:new{
|
|
text = _("Are you sure that you want to delete this file?\n") .. file .. ("\n") .. _("If you delete a file, it is permanently lost."),
|
|
ok_text = _("Delete"),
|
|
ok_callback = function()
|
|
deleteFile(file)
|
|
filemanagerutil.removeFileFromHistoryIfWanted(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 = util.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,
|
|
},
|
|
}},
|
|
}
|
|
fileManager.rename_dialog:onShowKeyboard()
|
|
UIManager:show(fileManager.rename_dialog)
|
|
end,
|
|
}
|
|
},
|
|
-- a little hack to get visual functionality grouping
|
|
{},
|
|
{
|
|
{
|
|
text = _("Convert"),
|
|
enabled = lfs.attributes(file, "mode") == "file"
|
|
and FileManagerConverter:isSupported(file),
|
|
callback = function()
|
|
UIManager:close(self.file_dialog)
|
|
FileManagerConverter:showConvertButtons(file, self)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Book information"),
|
|
enabled = FileManagerBookInfo:isSupported(file),
|
|
callback = function()
|
|
FileManagerBookInfo:show(file)
|
|
UIManager:close(self.file_dialog)
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
if lfs.attributes(file, "mode") == "directory" then
|
|
local realpath = util.realpath(file)
|
|
table.insert(buttons, {
|
|
{
|
|
text = _("Set as HOME directory"),
|
|
callback = function()
|
|
setHome(realpath)
|
|
UIManager:close(self.file_dialog)
|
|
end
|
|
}
|
|
})
|
|
end
|
|
|
|
self.file_dialog = ButtonDialogTitle:new{
|
|
title = file:match("([^/]+)$"),
|
|
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, ReaderDictionary:new{ ui = self })
|
|
table.insert(self, ReaderWikipedia: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
|
|
table.insert(self, plugin_or_err)
|
|
logger.info("FM loaded plugin", plugin_module.name,
|
|
"at", plugin_module.path)
|
|
end
|
|
end
|
|
end
|
|
|
|
if Device:hasKeys() then
|
|
self.key_events.Close = { {"Home"}, doc = "Close file manager" }
|
|
end
|
|
|
|
self:handleEvent(Event:new("SetDimensions", self.dimen))
|
|
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:init()
|
|
self.file_chooser.path_items = path_items_backup
|
|
self:onRefresh()
|
|
if focused_file then
|
|
self.file_chooser:changePageToPath(focused_file)
|
|
end
|
|
end
|
|
|
|
function FileManager:toggleHiddenFiles()
|
|
self.file_chooser:toggleHiddenFiles()
|
|
G_reader_settings:saveSetting("show_hidden", self.file_chooser.show_hidden)
|
|
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")
|
|
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 home_dir then
|
|
self.file_chooser:changeToPath(home_dir)
|
|
else
|
|
self:setHome()
|
|
end
|
|
return true
|
|
end
|
|
|
|
function FileManager:setHome(path)
|
|
path = path or self.file_chooser.path
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
UIManager:show(ConfirmBox:new{
|
|
text = util.template(_("Set '%1' as HOME directory?"), path),
|
|
ok_text = _("Set as HOME"),
|
|
ok_callback = function()
|
|
G_reader_settings:saveSetting("home_dir", path)
|
|
end,
|
|
})
|
|
return true
|
|
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 = util.realpath(file)
|
|
local orig = util.realpath(self.clipboard)
|
|
local dest = lfs.attributes(file, "mode") == "directory" and
|
|
file or file:match("(.*/)")
|
|
if self.cutfile then
|
|
-- 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
|
|
self:moveFile(orig, dest)
|
|
else
|
|
util.execute(self.cp_bin, "-r", orig, dest)
|
|
end
|
|
end
|
|
end
|
|
|
|
function FileManager:deleteFile(file)
|
|
local ok, err
|
|
local file_abs_path = util.realpath(file)
|
|
if file_abs_path == nil then
|
|
UIManager:show(InfoMessage:new{
|
|
text = util.template(_("File %1 not found"), file),
|
|
})
|
|
return
|
|
end
|
|
|
|
local is_doc = DocumentRegistry:getProvider(file_abs_path)
|
|
ok, err = os.remove(file_abs_path)
|
|
if ok and err == nil then
|
|
if is_doc ~= nil then
|
|
DocSettings:open(file):purge()
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = util.template(_("Deleted %1"), file),
|
|
timeout = 2,
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = util.template(_("An error occurred while trying to delete %1"), file),
|
|
})
|
|
end
|
|
end
|
|
|
|
function FileManager:renameFile(file)
|
|
if util.basename(file) ~= self.rename_dialog:getInputText() then
|
|
local dest = util.joinPath(util.dirname(file), self.rename_dialog:getInputText())
|
|
if self:moveFile(file, dest) then
|
|
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 = util.template(_("Renamed from %1 to %2"), file, dest),
|
|
timeout = 2,
|
|
})
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = util.template(
|
|
_("Failed to move history data of %1 to %2.\nThe reading history may be lost."),
|
|
file, dest),
|
|
})
|
|
end
|
|
end
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = util.template(
|
|
_("Failed to rename from %1 to %2"), file, dest),
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
function FileManager:getSortingMenuTable()
|
|
local fm = self
|
|
local collates = {
|
|
strcoll = {_("title"), _("Sort by title")},
|
|
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")},
|
|
}
|
|
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
|
|
return {
|
|
text_func = function()
|
|
return util.template(
|
|
_("Sort by: %1"),
|
|
collates[fm.file_chooser.collate][1]
|
|
)
|
|
end,
|
|
sub_item_table = {
|
|
set_collate_table("strcoll"),
|
|
set_collate_table("access"),
|
|
set_collate_table("change"),
|
|
set_collate_table("modification"),
|
|
set_collate_table("size"),
|
|
set_collate_table("type"),
|
|
}
|
|
}
|
|
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")},
|
|
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 util.template(
|
|
_("Start with: %1"),
|
|
start_withs[start_with_setting][1]
|
|
)
|
|
end,
|
|
sub_item_table = {
|
|
set_sw_table("filemanager"),
|
|
set_sw_table("history"),
|
|
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(),
|
|
root_path = path,
|
|
onExit = function()
|
|
self.instance = nil
|
|
end
|
|
}
|
|
if focused_file then
|
|
file_manager.file_chooser:changePageToPath(focused_file)
|
|
end
|
|
UIManager:show(file_manager)
|
|
self.instance = file_manager
|
|
end
|
|
|
|
--[[
|
|
A shortcut to execute mv command (self.mv_bin) with from and to as parameters.
|
|
Returns a boolean value to indicate the result of mv command.
|
|
--]]
|
|
function FileManager:moveFile(from, to)
|
|
return util.execute(self.mv_bin, from, to) == 0
|
|
end
|
|
|
|
return FileManager
|