<commit_before>local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
local InputContainer = require("ui/widget/container/inputcontainer")
local FrameContainer = require("ui/widget/container/framecontainer")
local CenterContainer = require("ui/widget/container/centercontainer")
local FileManagerMenu = require("apps/filemanager/filemanagermenu")
local DocumentRegistry = require("document/documentregistry")
local VerticalGroup = require("ui/widget/verticalgroup")
local Screenshoter = require("ui/widget/screenshoter")
local ButtonDialog = require("ui/widget/buttondialog")
local InputDialog = require("ui/widget/inputdialog")
local VerticalSpan = require("ui/widget/verticalspan")
local FileChooser = require("ui/widget/filechooser")
local TextWidget = require("ui/widget/textwidget")
local Blitbuffer = require("ffi/blitbuffer")
local lfs = require("libs/libkoreader-lfs")
local DocSettings = require("docsettings")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local Geom = require("ui/geometry")
local Event = require("ui/event")
local Device = require("device")
local util = require("ffi/util")
local Font = require("ui/font")
local DEBUG = require("dbg")
local _ = require("gettext")
local KeyValuePage = require("ui/widget/keyvaluepage")
local ReaderUI = require("apps/reader/readerui")
local InfoMessage = require("ui/widget/infomessage")

local function getDefaultDir()
    if Device:isKindle() then
        return "/mnt/us/documents"
    elseif Device:isKobo() then
        return "/mnt/onboard"
    elseif Device:isAndroid() then
        return "/sdcard"
    else
        return "."
    end
end

local function abbreviate(path)
    local home_dir_name = G_reader_settings:readSetting("home_dir_display_name")
    if home_dir_name ~= nil then
        local home_dir = G_reader_settings:readSetting("home_dir") or getDefaultDir()
        local len = home_dir:len()
        local start = path:sub(1, len)
        if start == home_dir then
            return home_dir_name .. path:sub(len+1)
        end
    end
    return path
end

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 = _("File Manager"),
    root_path = lfs.currentdir(),
    -- our own size
    dimen = Geom:new{ w = 400, h = 600 },
    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

    self.path_text = TextWidget:new{
        face = Font:getFace("infofont", 18),
        text = abbreviate(self.root_path),
    }

    self.banner = FrameContainer:new{
        padding = 0,
        bordersize = 0,
        VerticalGroup:new{
            TextWidget:new{
                face = Font:getFace("tfont", 24),
                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(abbreviate(path))
        UIManager:setDirty(FileManager.instance, function()
            return "ui", FileManager.instance.banner.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 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 = _("Cut"),
                    callback = function()
                        cutFile(file)
                        UIManager:close(self.file_dialog)
                    end,
                },
                {
                    text = _("Delete"),
                    callback = function()
                        local ConfirmBox = require("ui/widget/confirmbox")
                        UIManager:close(self.file_dialog)
                        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_callback = function()
                                deleteFile(file)
                                self:refreshPath()
                            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 = _("OK"),
                                    enabled = true,
                                    callback = function()
                                        renameFile(file)
                                        self:refreshPath()
                                        UIManager:close(fileManager.rename_dialog)
                                    end,
                                },
                                {
                                    text = _("Cancel"),
                                    enabled = true,
                                    callback = function()
                                        UIManager:close(fileManager.rename_dialog)
                                    end,
                                },
                            }},
                        }
                        fileManager.rename_dialog:onShowKeyboard()
                        UIManager:show(fileManager.rename_dialog)
                    end,
                }
            },
            {
                {
                    text = _("Book information"),
                    enabled = lfs.attributes(file, "mode") == "file" and true or false,
                    callback = function()
                        local book_info_metadata = FileManager:bookInformation(file)
                        if  book_info_metadata then
                            UIManager:show(KeyValuePage:new{
                                title = _("Book information"),
                                kv_pairs = book_info_metadata,
                            })
                        else
                            UIManager:show(InfoMessage:new{
                                text = _("Cannot fetch information for a selected book"),
                            })
                        end
                        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()
                        G_reader_settings:saveSetting("home_dir", realpath)
                        UIManager:close(self.file_dialog)
                    end
                }
            })
        end
        self.file_dialog = ButtonDialog:new{
            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,
        menu = self.menu
    })

    if Device:hasKeys() then
        self.key_events.Close = { {"Home"}, doc = "Close file manager" }
    end

    self:handleEvent(Event:new("SetDimensions", self.dimen))
end

function FileManager:bookInformation(file)
    local file_mode = lfs.attributes(file, "mode")
    if file_mode ~= "file" then return false end
    local book_stats = DocSettings:open(file):readSetting('stats')
    if book_stats ~= nil then
        return FileManagerHistory:buildBookInformationTable(book_stats)
    end
    local document = DocumentRegistry:openDocument(file)
    if document.loadDocument then
        document:loadDocument()
        document:render()
    end
    book_stats = document:getProps()
    book_stats.pages = document:getPageCount()
    return FileManagerHistory:buildBookInformationTable(book_stats)
end

function FileManager:reinit(path)
    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()
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()
    DEBUG("close filemanager")
    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: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
            self:moveFile(orig, dest)
        else
            util.execute(self.cp_bin, "-r", orig, dest)
        end
    end
end
<commit_msg>FileManager: added button to purge sidecar directory (#2391)<commit_after>local FileManagerHistory = require("apps/filemanager/filemanagerhistory")
local InputContainer = require("ui/widget/container/inputcontainer")
local FrameContainer = require("ui/widget/container/framecontainer")
local CenterContainer = require("ui/widget/container/centercontainer")
local FileManagerMenu = require("apps/filemanager/filemanagermenu")
local DocumentRegistry = require("document/documentregistry")
local VerticalGroup = require("ui/widget/verticalgroup")
local Screenshoter = require("ui/widget/screenshoter")
local ButtonDialog = require("ui/widget/buttondialog")
local InputDialog = require("ui/widget/inputdialog")
local VerticalSpan = require("ui/widget/verticalspan")
local FileChooser = require("ui/widget/filechooser")
local TextWidget = require("ui/widget/textwidget")
local Blitbuffer = require("ffi/blitbuffer")
local lfs = require("libs/libkoreader-lfs")
local DocSettings = require("docsettings")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local Geom = require("ui/geometry")
local Event = require("ui/event")
local Device = require("device")
local util = require("ffi/util")
local Font = require("ui/font")
local DEBUG = require("dbg")
local _ = require("gettext")
local KeyValuePage = require("ui/widget/keyvaluepage")
local ReaderUI = require("apps/reader/readerui")
local InfoMessage = require("ui/widget/infomessage")

local function getDefaultDir()
    if Device:isKindle() then
        return "/mnt/us/documents"
    elseif Device:isKobo() then
        return "/mnt/onboard"
    elseif Device:isAndroid() then
        return "/sdcard"
    else
        return "."
    end
end

local function abbreviate(path)
    local home_dir_name = G_reader_settings:readSetting("home_dir_display_name")
    if home_dir_name ~= nil then
        local home_dir = G_reader_settings:readSetting("home_dir") or getDefaultDir()
        local len = home_dir:len()
        local start = path:sub(1, len)
        if start == home_dir then
            return home_dir_name .. path:sub(len+1)
        end
    end
    return path
end

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 = _("File Manager"),
    root_path = lfs.currentdir(),
    -- our own size
    dimen = Geom:new{ w = 400, h = 600 },
    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

    self.path_text = TextWidget:new{
        face = Font:getFace("infofont", 18),
        text = abbreviate(self.root_path),
    }

    self.banner = FrameContainer:new{
        padding = 0,
        bordersize = 0,
        VerticalGroup:new{
            TextWidget:new{
                face = Font:getFace("tfont", 24),
                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(abbreviate(path))
        UIManager:setDirty(FileManager.instance, function()
            return "ui", FileManager.instance.banner.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 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:hasSidecarDir(util.realpath(file)),
                    callback = function()
                        local full_path = util.realpath(file)
                        util.purgeDir(DocSettings:getSidecarDir(full_path))
                        self:refreshPath()
                        -- also remove from history if present
                        local readhistory = require("readhistory")
                        for _, hist_item in ipairs(readhistory.hist) do
                            if hist_item.file == full_path then
                                readhistory:removeItem(hist_item)
                                break
                            end
                        end
                        UIManager:close(self.file_dialog)
                    end,
                },
            },
            {
                {
                    text = _("Cut"),
                    callback = function()
                        cutFile(file)
                        UIManager:close(self.file_dialog)
                    end,
                },
                {
                    text = _("Delete"),
                    callback = function()
                        local ConfirmBox = require("ui/widget/confirmbox")
                        UIManager:close(self.file_dialog)
                        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_callback = function()
                                deleteFile(file)
                                self:refreshPath()
                            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 = _("OK"),
                                    enabled = true,
                                    callback = function()
                                        renameFile(file)
                                        self:refreshPath()
                                        UIManager:close(fileManager.rename_dialog)
                                    end,
                                },
                                {
                                    text = _("Cancel"),
                                    enabled = true,
                                    callback = function()
                                        UIManager:close(fileManager.rename_dialog)
                                    end,
                                },
                            }},
                        }
                        fileManager.rename_dialog:onShowKeyboard()
                        UIManager:show(fileManager.rename_dialog)
                    end,
                }
            },
            {
                {
                    text = _("Book information"),
                    enabled = lfs.attributes(file, "mode") == "file" and true or false,
                    callback = function()
                        local book_info_metadata = FileManager:bookInformation(file)
                        if  book_info_metadata then
                            UIManager:show(KeyValuePage:new{
                                title = _("Book information"),
                                kv_pairs = book_info_metadata,
                            })
                        else
                            UIManager:show(InfoMessage:new{
                                text = _("Cannot fetch information for a selected book"),
                            })
                        end
                        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()
                        G_reader_settings:saveSetting("home_dir", realpath)
                        UIManager:close(self.file_dialog)
                    end
                }
            })
        end
        self.file_dialog = ButtonDialog:new{
            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,
        menu = self.menu
    })

    if Device:hasKeys() then
        self.key_events.Close = { {"Home"}, doc = "Close file manager" }
    end

    self:handleEvent(Event:new("SetDimensions", self.dimen))
end

function FileManager:bookInformation(file)
    local file_mode = lfs.attributes(file, "mode")
    if file_mode ~= "file" then return false end
    local book_stats = DocSettings:open(file):readSetting('stats')
    if book_stats ~= nil then
        return FileManagerHistory:buildBookInformationTable(book_stats)
    end
    local document = DocumentRegistry:openDocument(file)
    if document.loadDocument then
        document:loadDocument()
        document:render()
    end
    book_stats = document:getProps()
    book_stats.pages = document:getPageCount()
    return FileManagerHistory:buildBookInformationTable(book_stats)
end

function FileManager:reinit(path)
    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()
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()
    DEBUG("close filemanager")
    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: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:hasSidecarDir(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

{'fraction_non_alphanumeric': 0.09117854099514654, 'fraction_numerical': 0.0015560742469712125, 'mean_word_length': 1.7176802255336288, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 28, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4614180', 'n_tokens_mistral': 7191, 'n_tokens_neox': 6971, 'n_words': 1496}