From a703b213f76e660be60f2b65601967f0f81c3410 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Thu, 16 Dec 2021 13:12:25 +0200 Subject: [PATCH] File manager: group operations (#8536) Copy/Move/Delete for group of files. "Select files" button in the filemanager Plus menu. --- frontend/apps/filemanager/filemanager.lua | 404 +++++++++++++------ frontend/dispatcher.lua | 2 + frontend/ui/widget/filechooser.lua | 18 +- frontend/ui/widget/iconbutton.lua | 7 + frontend/ui/widget/menu.lua | 2 +- plugins/coverbrowser.koplugin/covermenu.lua | 1 + plugins/coverbrowser.koplugin/listmenu.lua | 6 +- plugins/coverbrowser.koplugin/mosaicmenu.lua | 6 +- 8 files changed, 305 insertions(+), 141 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 3563d7960..2096f9b0a 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -44,12 +44,16 @@ local BaseUtil = require("ffi/util") local util = require("util") local _ = require("gettext") local C_ = _.pgettext +local N_ = _.ngettext local Screen = Device.screen local T = BaseUtil.template local FileManager = InputContainer:extend{ title = _("KOReader"), root_path = lfs.currentdir(), + + clipboard = nil, -- for single file operations + selected_files = nil, -- for group file operations mv_bin = Device:isAndroid() and "/system/bin/mv" or "/bin/mv", cp_bin = Device:isAndroid() and "/system/bin/cp" or "/bin/cp", @@ -61,6 +65,9 @@ function FileManager:onSetRotationMode(rotation) Screen:setRotationMode(rotation) if FileManager.instance then self:reinit(self.path, self.focused_file) + if self.select_mode then + self.plus_button:setIcon("check") + end UIManager:setDirty(self.banner, function() return "ui", self.banner.dimen end) @@ -121,13 +128,13 @@ function FileManager:setupLayout() hold_callback = function() self:setHome() end, } - local plus_button = IconButton:new{ + self.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_right = Size.padding.default, padding_bottom = 0, callback = function() self:onShowPlusMenu() end, } @@ -159,7 +166,7 @@ function FileManager:setupLayout() width = Screen:getWidth() - 2 * icon_size - 4 * Size.padding.large, }, }, - plus_button, + self.plus_button, } }, CenterContainer:new{ @@ -200,22 +207,33 @@ function FileManager:setupLayout() -- 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, + filemanager = self, } self.file_chooser = file_chooser self.focused_file = nil -- use it only once + local file_manager = self + 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 + file_manager.path_text:setText(BD.directory(filemanagerutil.abbreviate(path))) + UIManager:setDirty(file_manager, function() + return "ui", file_manager.path_text.dimen, file_manager.dithered end) return true end function file_chooser:onFileSelect(file) -- luacheck: ignore - local ReaderUI = require("apps/reader/readerui") - ReaderUI:showReader(file) + if file_manager.select_mode then + if file_manager.selected_files[file] then + file_manager.selected_files[file] = nil + else + file_manager.selected_files[file] = true + end + self:refreshPath() + else + local ReaderUI = require("apps/reader/readerui") + ReaderUI:showReader(file) + end return true end @@ -225,9 +243,9 @@ function FileManager:setupLayout() 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 + if file_manager.select_mode then return true end local is_file = lfs.attributes(file, "mode") == "file" local is_folder = lfs.attributes(file, "mode") == "directory" local is_not_parent_folder = BaseUtil.basename(file) ~= ".." @@ -243,7 +261,7 @@ function FileManager:setupLayout() }, { text = C_("File", "Paste"), - enabled = fileManager.clipboard and true or false, + enabled = file_manager.clipboard and true or false, callback = function() pasteHere(file) UIManager:close(self.file_dialog) @@ -297,7 +315,7 @@ function FileManager:setupLayout() enabled = is_not_parent_folder, callback = function() UIManager:close(self.file_dialog) - fileManager.rename_dialog = InputDialog:new{ + file_manager.rename_dialog = InputDialog:new{ title = is_file and _("Rename file") or _("Rename folder"), input = BaseUtil.basename(file), buttons = {{ @@ -305,23 +323,23 @@ function FileManager:setupLayout() text = _("Cancel"), enabled = true, callback = function() - UIManager:close(fileManager.rename_dialog) + UIManager:close(file_manager.rename_dialog) end, }, { text = _("Rename"), enabled = true, callback = function() - if fileManager.rename_dialog:getInputText() ~= "" then + if file_manager.rename_dialog:getInputText() ~= "" then renameFile(file) - UIManager:close(fileManager.rename_dialog) + UIManager:close(file_manager.rename_dialog) end end, }, }}, } - UIManager:show(fileManager.rename_dialog) - fileManager.rename_dialog:onShowKeyboard() + UIManager:show(file_manager.rename_dialog) + file_manager.rename_dialog:onShowKeyboard() end, } }, @@ -375,19 +393,19 @@ function FileManager:setupLayout() table.insert(buttons, { { text = _("Open with…"), - enabled = DocumentRegistry:getProviders(file) == nil or #(DocumentRegistry:getProviders(file)) > 1 or fileManager.texteditor, + enabled = DocumentRegistry:getProviders(file) == nil or #(DocumentRegistry:getProviders(file)) > 1 or file_manager.texteditor, callback = function() UIManager:close(self.file_dialog) local one_time_providers = {} - if fileManager.texteditor then + if file_manager.texteditor then table.insert(one_time_providers, { provider_name = _("Text editor"), callback = function() - fileManager.texteditor:checkEditFile(file) + file_manager.texteditor:checkEditFile(file) end, }) end - self:showSetProviderButtons(file, FileManager.instance, one_time_providers) + self:showSetProviderButtons(file, one_time_providers) end, }, { @@ -580,11 +598,6 @@ function FileChooser:onBack() end end -function FileManager:onShowPlusMenu() - self:tapPlus() - return true -end - function FileManager:onSwipeFM(ges) local direction = BD.flipDirectionIfMirroredUILayout(ges.direction) if direction == "west" then @@ -595,135 +608,258 @@ function FileManager:onSwipeFM(ges) return true end +function FileManager:onShowPlusMenu() + self:tapPlus() + return true +end + +function FileManager:onToggleSelectMode() + logger.dbg("toggle select mode") + self.select_mode = not self.select_mode + self.selected_files = self.select_mode and {} or nil + self.plus_button:setIcon(self.select_mode and "check" or "plus") + UIManager:setDirty(self, function() + return "ui", self.plus_button.dimen, self.dithered + end) + self:onRefresh() +end + function FileManager:tapPlus() - local buttons = { - { + local title, buttons + if self.select_mode then + local select_count = util.tableSize(self.selected_files) + local actions_enabled = select_count > 0 + title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count) + or _("No files selected") + buttons = { { - text = _("New folder"), - callback = function() - UIManager:close(self.file_dialog) - self.input_dialog = InputDialog:new{ - title = _("New folder"), - input_type = "text", - buttons = { - { - { - text = _("Cancel"), - callback = function() - self:closeInputDialog() - end, - }, - { - text = _("Create"), - callback = function() - local new_folder = self.input_dialog:getInputText() - if new_folder and new_folder ~= "" then - self:createFolder(self.file_chooser.path, new_folder) - self:closeInputDialog() - end - end, - }, - } - }, - } - UIManager:show(self.input_dialog) - self.input_dialog:onShowKeyboard() - end, + { + text = _("Select all files in folder"), + callback = function() + self.file_chooser:selectAllFilesInFolder() + self:onRefresh() + UIManager:close(self.file_dialog) + end, + }, + { + text = _("Copy"), + enabled = actions_enabled, + callback = function() + UIManager:show(ConfirmBox:new{ + text = _("Copy selected files to the current folder?"), + ok_text = _("Copy"), + ok_callback = function() + self.cutfile = false + for file in pairs(self.selected_files) do + self.clipboard = file + self:pasteHere(self.file_chooser.path) + end + self:onToggleSelectMode() + UIManager:close(self.file_dialog) + end, + }) + 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 = _("Deselect all"), + enabled = actions_enabled, + callback = function() + self.selected_files = {} + self:onRefresh() + UIManager:close(self.file_dialog) + end, + }, + { + text = _("Move"), + enabled = actions_enabled, + callback = function() + UIManager:show(ConfirmBox:new{ + text = _("Move selected files to the current folder?"), + ok_text = _("Move"), + ok_callback = function() + self.cutfile = true + for file in pairs(self.selected_files) do + self.clipboard = file + self:pasteHere(self.file_chooser.path) + end + self:onToggleSelectMode() + UIManager:close(self.file_dialog) + end, + }) + 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 = _("Exit select mode"), + callback = function() + self:onToggleSelectMode() + UIManager:close(self.file_dialog) + end, + }, + { + text = _("Delete"), + enabled = actions_enabled, + callback = function() + UIManager:show(ConfirmBox:new{ + text = _("Delete selected files?\nIf you delete a file, it is permanently lost."), + ok_text = _("Delete"), + ok_callback = function() + local readhistory = require("readhistory") + for file in pairs(self.selected_files) do + self:deleteFile(file) + readhistory:fileDeleted(file) + end + self:onToggleSelectMode() + UIManager:close(self.file_dialog) + end, + }) + end + }, + }, + } + else + title = BD.dirpath(filemanagerutil.abbreviate(self.file_chooser.path)) + buttons = { { - text = _("Open random document"), - callback = function() - self:openRandomFile(self.file_chooser.path) - UIManager:close(self.file_dialog) - end - } - }, - { + { + text = _("Select files"), + callback = function() + self:onToggleSelectMode() + 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 = _("New folder"), + callback = function() + UIManager:close(self.file_dialog) + self.input_dialog = InputDialog:new{ + title = _("New folder"), + buttons = { + { + { + text = _("Cancel"), + callback = function() + self:closeInputDialog() + end, + }, + { + text = _("Create"), + callback = function() + local new_folder = self.input_dialog:getInputText() + if new_folder and new_folder ~= "" then + self:createFolder(self.file_chooser.path, new_folder) + self:closeInputDialog() + end + end, + }, + } + }, + } + UIManager:show(self.input_dialog) + self.input_dialog:onShowKeyboard() + end, + }, + }, { - 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, + { + 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, + }, }, - }) - 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") + { + text = _("Set as HOME folder"), + callback = function() + self:setHome(self.file_chooser.path) + UIManager:close(self.file_dialog) end - end, - callback = function() - if Device:isValidPath(self.file_chooser.path) then - local ok, sd_path = Device:hasExternalSD() + } + }, + { + { + text = _("Go to HOME folder"), + callback = function() + self:goHome() UIManager:close(self.file_dialog) - if ok then - self.file_chooser:changeToPath(sd_path) - end - else + end + } + }, + { + { + text = _("Open random document"), + callback = function() + self:openRandomFile(self.file_chooser.path) UIManager:close(self.file_dialog) - self.file_chooser:changeToPath(Device.home_dir) end - end, + } }, - }) + { + { + text = _("Folder shortcuts"), + callback = function() + self:handleEvent(Event:new("ShowFolderShortcutsDialog")) + UIManager:close(self.file_dialog) + end + } + } + } + + if Device:canImportFiles() then + table.insert(buttons, 4, { + { + 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, 5, { + { + 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 end self.file_dialog = ButtonDialogTitle:new{ - title = BD.dirpath(filemanagerutil.abbreviate(self.file_chooser.path)), + title = title, title_align = "center", buttons = buttons, + select_mode = self.select_mode, -- for coverbrowser } UIManager:show(self.file_dialog) end diff --git a/frontend/dispatcher.lua b/frontend/dispatcher.lua index c7aa5bed0..24af0a751 100644 --- a/frontend/dispatcher.lua +++ b/frontend/dispatcher.lua @@ -97,6 +97,7 @@ local settingsList = { -- filemanager settings folder_up = {category="none", event="FolderUp", title=_("Folder up"), filemanager=true}, show_plus_menu = {category="none", event="ShowPlusMenu", title=_("Show plus menu"), filemanager=true}, + toggle_select_mode = {category="none", event="ToggleSelectMode", title=_("Toggle select mode"), filemanager=true}, refresh_content = {category="none", event="RefreshContent", title=_("Refresh content"), filemanager=true}, folder_shortcuts = {category="none", event="ShowFolderShortcutsDialog", title=_("Folder shortcuts"), filemanager=true, separator=true}, @@ -254,6 +255,7 @@ local dispatcher_menu_order = { -- filemanager "folder_up", "show_plus_menu", + "toggle_select_mode", "refresh_content", "folder_shortcuts", diff --git a/frontend/ui/widget/filechooser.lua b/frontend/ui/widget/filechooser.lua index 91b87fdae..84bb7b429 100644 --- a/frontend/ui/widget/filechooser.lua +++ b/frontend/ui/widget/filechooser.lua @@ -324,7 +324,8 @@ function FileChooser:genItemTableFromPath(path) text = file.name, bidi_wrap_func = BD.filename, mandatory = sstr, - path = full_path + path = full_path, + is_file = true, } if show_file_in_bold ~= false then file_item.bold = DocSettings:hasSidecarFile(full_path) @@ -332,6 +333,9 @@ function FileChooser:genItemTableFromPath(path) file_item.bold = not file_item.bold end end + if self.filemanager and self.filemanager.selected_files and self.filemanager.selected_files[full_path] then + file_item.dim = true + end table.insert(item_table, file_item) end @@ -492,7 +496,17 @@ function FileChooser:getNextFile(curr_file) return next_file end -function FileChooser:showSetProviderButtons(file, filemanager_instance, one_time_providers) +-- Used in file manager select mode to select all files in a folder, +-- that are visible in all file browser pages, without subfolders. +function FileChooser:selectAllFilesInFolder() + for _, item in pairs(self.item_table) do + if item.is_file then + self.filemanager.selected_files[item.path] = true + end + end +end + +function FileChooser:showSetProviderButtons(file, one_time_providers) local ReaderUI = require("apps/reader/readerui") local __, filename_pure = util.splitFilePathName(file) diff --git a/frontend/ui/widget/iconbutton.lua b/frontend/ui/widget/iconbutton.lua index cc691d2ea..10a0be524 100644 --- a/frontend/ui/widget/iconbutton.lua +++ b/frontend/ui/widget/iconbutton.lua @@ -158,4 +158,11 @@ function IconButton:onTapSelect() self:onTapIconButton() end +function IconButton:setIcon(icon) + if icon ~= self.icon then + self.icon = icon + self:init() + end +end + return IconButton diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 01b6081fc..a9cffe148 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -994,7 +994,7 @@ function Menu:init() } end -- delegate swipe gesture to GestureManager in filemanager - if self.is_file_manager ~= true then + if not self.filemanager then self.ges_events.Swipe = { GestureRange:new{ ges = "swipe", diff --git a/plugins/coverbrowser.koplugin/covermenu.lua b/plugins/coverbrowser.koplugin/covermenu.lua index 4c1fac626..bfe55c3dc 100644 --- a/plugins/coverbrowser.koplugin/covermenu.lua +++ b/plugins/coverbrowser.koplugin/covermenu.lua @@ -698,6 +698,7 @@ function CoverMenu:tapPlus() -- Call original function: it will create a ButtonDialogTitle -- and store it as self.file_dialog, and UIManager:show() it. CoverMenu._FileManager_tapPlus_orig(self) + if self.file_dialog.select_mode then return end -- do not change select menu -- Remember some of this original ButtonDialogTitle properties local orig_title = self.file_dialog.title diff --git a/plugins/coverbrowser.koplugin/listmenu.lua b/plugins/coverbrowser.koplugin/listmenu.lua index f4f026535..8aea21e0b 100644 --- a/plugins/coverbrowser.koplugin/listmenu.lua +++ b/plugins/coverbrowser.koplugin/listmenu.lua @@ -262,8 +262,10 @@ function ListMenuItem:update() }, } else - if file_mode ~= "file" then - self.file_deleted = true + local is_file_selected = self.menu.filemanager and self.menu.filemanager.selected_files + and self.menu.filemanager.selected_files[self.filepath] + if file_mode ~= "file" or is_file_selected then + self.file_deleted = true -- dim file end -- File diff --git a/plugins/coverbrowser.koplugin/mosaicmenu.lua b/plugins/coverbrowser.koplugin/mosaicmenu.lua index 20aad959a..9a2b3942a 100644 --- a/plugins/coverbrowser.koplugin/mosaicmenu.lua +++ b/plugins/coverbrowser.koplugin/mosaicmenu.lua @@ -521,8 +521,10 @@ function MosaicMenuItem:update() }, } else - if file_mode ~= "file" then - self.file_deleted = true + local is_file_selected = self.menu.filemanager and self.menu.filemanager.selected_files + and self.menu.filemanager.selected_files[self.filepath] + if file_mode ~= "file" or is_file_selected then + self.file_deleted = true -- dim file end -- File : various appearances