mirror of
https://github.com/koreader/koreader
synced 2024-11-16 06:12:56 +00:00
14f9f9d3fc
Tweak the goHome handler directly, instead of only the Home button. Also, limit it to when it's actually needed, as changeToPath already does it.
1159 lines
43 KiB
Lua
1159 lines
43 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 DeviceListener = require("device/devicelistener")
|
||
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 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 = BaseUtil.template
|
||
|
||
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:onSetRotationMode(rotation)
|
||
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
|
||
Screen:setRotationMode(rotation)
|
||
if self.instance then
|
||
self:reinit(self.instance.path, self.instance.focused_file)
|
||
UIManager:setDirty(self.instance.banner, function()
|
||
return "ui", self.instance.banner.dimen
|
||
end)
|
||
end
|
||
end
|
||
return true
|
||
end
|
||
|
||
function FileManager:setRotationMode()
|
||
local locked = G_reader_settings:isTrue("lock_rotation")
|
||
if not locked then
|
||
local rotation_mode = G_reader_settings:readSetting("fm_rotation_mode") or Screen.ORIENTATION_PORTRAIT
|
||
self:onSetRotationMode(rotation_mode)
|
||
end
|
||
end
|
||
|
||
function FileManager:initGesListener()
|
||
if not Device:isTouchDevice() then
|
||
return
|
||
end
|
||
|
||
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
|
||
|
||
function FileManager:onSetDimensions(dimen)
|
||
-- update listening according to new screen dimen
|
||
if Device:isTouchDevice() then
|
||
self:updateTouchZonesOnScreenResize(dimen)
|
||
end
|
||
end
|
||
|
||
function FileManager:setupLayout()
|
||
self.show_parent = self.show_parent or self
|
||
local icon_size = Screen:scaleBySize(DGENERIC_ICON_SIZE)
|
||
local home_button = IconButton:new{
|
||
icon = "home",
|
||
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 = "plus",
|
||
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:onShowPlusMenu() end,
|
||
hold_callback = function()
|
||
self:onRefresh()
|
||
UIManager:show(InfoMessage:new{
|
||
text = _("Content refreshed."),
|
||
timeout = 2,
|
||
})
|
||
end,
|
||
}
|
||
|
||
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 show_hidden
|
||
if G_reader_settings:has("show_hidden") then
|
||
show_hidden = G_reader_settings:isTrue("show_hidden")
|
||
else
|
||
show_hidden = DSHOWHIDDENFILES
|
||
end
|
||
local show_unsupported = G_reader_settings:isTrue("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,
|
||
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 is_file = lfs.attributes(file, "mode") == "file"
|
||
local is_folder = lfs.attributes(file, "mode") == "directory"
|
||
local is_not_parent_folder = BaseUtil.basename(file) ~= ".."
|
||
local buttons = {
|
||
{
|
||
{
|
||
text = C_("File", "Copy"),
|
||
enabled = is_not_parent_folder,
|
||
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"),
|
||
enabled = is_not_parent_folder,
|
||
callback = function()
|
||
cutFile(file)
|
||
UIManager:close(self.file_dialog)
|
||
end,
|
||
},
|
||
{
|
||
text = _("Delete"),
|
||
enabled = is_not_parent_folder,
|
||
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"),
|
||
enabled = is_not_parent_folder,
|
||
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 is_file and Device:canExecuteScript(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
|
||
if Device:isAndroid() then
|
||
Device:setIgnoreInput(true)
|
||
rv = os.execute("sh " .. BaseUtil.realpath(file)) -- run by sh, because sdcard has no execute permissions
|
||
Device:setIgnoreInput(false)
|
||
else
|
||
rv = os.execute(BaseUtil.realpath(file))
|
||
end
|
||
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 = "notice-warning",
|
||
})
|
||
end
|
||
end)
|
||
end,
|
||
}
|
||
)
|
||
end
|
||
|
||
if is_file then
|
||
table.insert(buttons, {
|
||
{
|
||
text = _("Open with…"),
|
||
enabled = DocumentRegistry:getProviders(file) == nil or #(DocumentRegistry:getProviders(file)) > 1 or fileManager.texteditor,
|
||
callback = function()
|
||
UIManager:close(self.file_dialog)
|
||
local one_time_providers = {}
|
||
if fileManager.texteditor then
|
||
table.insert(one_time_providers, {
|
||
provider_name = _("Text editor"),
|
||
callback = function()
|
||
fileManager.texteditor:checkEditFile(file)
|
||
end,
|
||
})
|
||
end
|
||
self:showSetProviderButtons(file, FileManager.instance, ReaderUI, one_time_providers)
|
||
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 is_folder then
|
||
local realpath = BaseUtil.realpath(file)
|
||
table.insert(buttons, {
|
||
{
|
||
text = _("Set as HOME folder"),
|
||
callback = function()
|
||
setHome(realpath)
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
})
|
||
end
|
||
|
||
local title
|
||
if is_folder 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
|
||
}
|
||
|
||
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" }
|
||
if not Device:hasFewKeys() then
|
||
-- Also remove the handler assigned to the "Back" key by menu.lua
|
||
self.file_chooser.key_events.Close = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
function FileManager:init()
|
||
self:setupLayout()
|
||
|
||
local screenshoter = Screenshoter:new{ prefix = 'FileManager' }
|
||
table.insert(self, screenshoter) -- for regular events
|
||
self.active_widgets = { screenshoter } -- to get events even when hidden
|
||
|
||
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 })
|
||
table.insert(self, DeviceListener: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:hasWifiToggle() then
|
||
local NetworkListener = require("ui/network/networklistener")
|
||
table.insert(self, NetworkListener:new{ ui = self })
|
||
end
|
||
|
||
self:initGesListener()
|
||
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 folder"),
|
||
callback = function()
|
||
self:setHome(self.file_chooser.path)
|
||
UIManager:close(self.file_dialog)
|
||
end
|
||
}
|
||
},
|
||
{
|
||
{
|
||
text = _("Go to HOME folder"),
|
||
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
|
||
|
||
if Device:hasExternalSD() then
|
||
table.insert(buttons, 4, {
|
||
{
|
||
text_func = function()
|
||
if Device:isValidPath(self.file_chooser.path) then
|
||
return _("Switch to SDCard")
|
||
else
|
||
return _("Switch to internal storage")
|
||
end
|
||
end,
|
||
callback = function()
|
||
if Device:isValidPath(self.file_chooser.path) then
|
||
local ok, sd_path = Device:hasExternalSD()
|
||
UIManager:close(self.file_dialog)
|
||
if ok then
|
||
self.file_chooser:changeToPath(sd_path)
|
||
end
|
||
else
|
||
UIManager:close(self.file_dialog)
|
||
self.file_chooser:changeToPath(Device.home_dir)
|
||
end
|
||
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)
|
||
UIManager:flushSettings()
|
||
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:setupLayout()
|
||
self:handleEvent(Event:new("SetDimensions", self.dimen))
|
||
self.file_chooser.path_items = path_items_backup
|
||
-- self:init() has already done file_chooser:refreshPath()
|
||
-- (by virtue of rebuilding file_chooser), so this one
|
||
-- looks unnecessary (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)
|
||
-- Also pick up new content, if any.
|
||
self.file_chooser:refreshPath()
|
||
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 folder?"), 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 folder %1 already exists. Overwrite folder?"), 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")},
|
||
numeric = {_("numeric"), _("Sort by filename (natural sorting)")},
|
||
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("numeric"),
|
||
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")},
|
||
favorites = {_("favorites"), _("Start with favorites")},
|
||
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("favorites"),
|
||
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)
|
||
self:setRotationMode()
|
||
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
|