From aabd6d7a2602e75d7e12d4d8e48f291f7e11275e Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Sun, 10 Dec 2023 08:05:34 +0200 Subject: [PATCH] File browser, Collection: improve group actions (#11178) Maintain correct records in History and Favorites when moving/deleting folders or group of files. Optimize Collection module to minimize storage requests. --- frontend/apps/filemanager/filemanager.lua | 279 ++++++++++------ .../filemanager/filemanagercollection.lua | 121 +++---- .../apps/filemanager/filemanagershortcuts.lua | 2 +- frontend/apps/filemanager/filemanagerutil.lua | 6 +- frontend/readcollection.lua | 310 +++++++++++------- frontend/readhistory.lua | 52 ++- spec/unit/filemanager_spec.lua | 2 +- 7 files changed, 486 insertions(+), 286 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 6bbc430a0..f9f40deb6 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -57,6 +57,10 @@ local FileManager = InputContainer:extend{ cp_bin = Device:isAndroid() and "/system/bin/cp" or "/bin/cp", } +local function isFile(file) + return lfs.attributes(file, "mode") == "file" +end + function FileManager:onSetRotationMode(rotation) if rotation ~= nil and rotation ~= Screen:getRotationMode() then Screen:setRotationMode(rotation) @@ -190,7 +194,7 @@ function FileManager:setupLayout() end function file_chooser:showFileDialog(file) -- luacheck: ignore - local is_file = lfs.attributes(file, "mode") == "file" + local is_file = isFile(file) local is_folder = lfs.attributes(file, "mode") == "directory" local is_not_parent_folder = BaseUtil.basename(file) ~= ".." @@ -220,7 +224,7 @@ function FileManager:setupLayout() enabled = file_manager.clipboard and true or false, callback = function() UIManager:close(self.file_dialog) - file_manager:pasteHere(file) + file_manager:pasteFileFromClipboard(file) end, }, { @@ -514,20 +518,9 @@ function FileManager:tapPlus() 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() - UIManager:close(self.file_dialog) - self.cutfile = false - for file in pairs(self.selected_files) do - self.clipboard = file - self:pasteHere() - end - self:onToggleSelectMode() - end, - }) - end + self.cutfile = false + self:showCopyMoveSelectedFilesDialog(close_dialog_callback) + end, }, }, { @@ -543,20 +536,9 @@ function FileManager:tapPlus() 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() - UIManager:close(self.file_dialog) - self.cutfile = true - for file in pairs(self.selected_files) do - self.clipboard = file - self:pasteHere() - end - self:onToggleSelectMode() - end, - }) - end + self.cutfile = true + self:showCopyMoveSelectedFilesDialog(close_dialog_callback) + end, }, }, { @@ -565,7 +547,9 @@ function FileManager:tapPlus() enabled = actions_enabled, callback = function() UIManager:close(self.file_dialog) - self.selected_files = {} + for file in pairs (self.selected_files) do + self.selected_files[file] = nil + end self:onRefresh() end, }, @@ -578,10 +562,7 @@ function FileManager:tapPlus() ok_text = _("Delete"), ok_callback = function() UIManager:close(self.file_dialog) - for file in pairs(self.selected_files) do - self:deleteFile(file, true) -- only files can be selected - end - self:onToggleSelectMode() + self:deleteSelectedFiles() end, }) end, @@ -642,7 +623,7 @@ function FileManager:tapPlus() enabled = self.clipboard and true or false, callback = function() UIManager:close(self.file_dialog) - self:pasteHere() + self:pasteFileFromClipboard() end, }, }, @@ -651,7 +632,7 @@ function FileManager:tapPlus() text = _("Set as HOME folder"), callback = function() UIManager:close(self.file_dialog) - self:setHome(self.file_chooser.path) + self:setHome() end }, }, @@ -850,66 +831,137 @@ function FileManager:cutFile(file) self.clipboard = file end -function FileManager:pasteHere(file) +function FileManager:pasteFileFromClipboard(file) local orig_file = BaseUtil.realpath(self.clipboard) - local orig_name = BaseUtil.basename(self.clipboard) + local orig_name = BaseUtil.basename(orig_file) local dest_path = BaseUtil.realpath(file or self.file_chooser.path) - dest_path = lfs.attributes(dest_path, "mode") == "directory" and dest_path or dest_path:match("(.*/)") + dest_path = isFile(dest_path) and dest_path:match("(.*/)") or dest_path local dest_file = BaseUtil.joinPath(dest_path, orig_name) - local is_file = lfs.attributes(orig_file, "mode") == "file" + local is_file = isFile(orig_file) - local function infoCopyFile() - if self:copyRecursive(orig_file, dest_path) then - if is_file then - DocSettings.updateLocation(orig_file, dest_file, true) + local function doPaste() + local ok + if self.cutfile then + ok = self:moveFile(orig_file, dest_path) + else + ok = self:copyRecursive(orig_file, dest_path) + end + if ok then + if is_file then -- move or copy sdr + DocSettings.updateLocation(orig_file, dest_file, not self.cutfile) end - return true + if self.cutfile then -- for move only + if is_file then + ReadHistory:updateItem(orig_file, dest_file) + ReadCollection:updateItem(orig_file, dest_file) + else + ReadHistory:updateItemsByPath(orig_file, dest_file) + ReadCollection:updateItemsByPath(orig_file, dest_file) + end + end + self.clipboard = nil + self:onRefresh() else + local text = self.cutfile and "Failed to move:\n%1\nto:\n%2" + or "Failed to copy:\n%1\nto:\n%2" UIManager:show(InfoMessage:new{ - text = T(_("Failed to copy:\n%1\nto:\n%2"), BD.filepath(orig_name), BD.dirpath(dest_path)), + text = T(_(text), BD.filepath(orig_name), BD.dirpath(dest_path)), icon = "notice-warning", }) end end - local function infoMoveFile() - if self:moveFile(orig_file, dest_path) then - if is_file then - DocSettings.updateLocation(orig_file, dest_file) - ReadHistory:updateItemByPath(orig_file, dest_file) -- (will update "lastfile" if needed) - else - ReadHistory:updateItemsByPath(orig_file, dest_file) - end - ReadCollection:updateItemByPath(orig_file, dest_file) - return true + local mode_dest = lfs.attributes(dest_file, "mode") + if mode_dest then -- file or folder with target name already exists + local can_overwrite = (mode_dest == "file") == is_file + local text = can_overwrite == is_file and T(_("File already exists:\n%1"), BD.filename(orig_name)) + or T(_("Folder already exists:\n%1"), BD.directory(orig_name)) + if can_overwrite then + UIManager:show(ConfirmBox:new{ + text = text, + ok_text = _("Overwrite"), + ok_callback = function() + doPaste() + end, + }) else UIManager:show(InfoMessage:new{ - text = T(_("Failed to move:\n%1\nto:\n%2"), BD.filepath(orig_name), BD.dirpath(dest_path)), + text = text, icon = "notice-warning", }) end + else + doPaste() end +end - local function doPaste() - local ok = self.cutfile and infoMoveFile() or infoCopyFile() +function FileManager:showCopyMoveSelectedFilesDialog(close_callback) + local text, ok_text + if self.cutfile then + text = _("Move selected files to the current folder?") + ok_text = _("Move") + else + text = _("Copy selected files to the current folder?") + ok_text = _("Copy") + end + local confirmbox, check_button_overwrite + confirmbox = ConfirmBox:new{ + text = text, + ok_text = ok_text, + ok_callback = function() + close_callback() + self:pasteSelectedFiles(check_button_overwrite.checked) + end, + } + check_button_overwrite = CheckButton:new{ + text = _("overwrite existing files"), + checked = true, + parent = confirmbox, + } + confirmbox:addWidget(check_button_overwrite) + UIManager:show(confirmbox) +end + +function FileManager:pasteSelectedFiles(overwrite) + local dest_path = BaseUtil.realpath(self.file_chooser.path) + local ok_files = {} + for orig_file in pairs(self.selected_files) do + local orig_name = BaseUtil.basename(orig_file) + local dest_file = BaseUtil.joinPath(dest_path, orig_name) + local ok + local dest_mode = lfs.attributes(dest_file, "mode") + if not dest_mode or (dest_mode == "file" and overwrite) then + if self.cutfile then + ok = self:moveFile(orig_file, dest_path) + else + ok = self:copyRecursive(orig_file, dest_path) + end + end if ok then + DocSettings.updateLocation(orig_file, dest_file, not self.cutfile) + ok_files[orig_file] = true + self.selected_files[orig_file] = nil + end + end + local skipped_nb = util.tableSize(self.selected_files) + if util.tableSize(ok_files) > 0 then + if self.cutfile then -- for move only + ReadHistory:updateItems(ok_files, dest_path) + ReadCollection:updateItems(ok_files, dest_path) + end + if skipped_nb > 0 then self:onRefresh() - self.clipboard = nil end end - - local mode = lfs.attributes(dest_file, "mode") - if mode then - UIManager:show(ConfirmBox:new{ - text = mode == "file" and T(_("File already exists:\n%1\nOverwrite file?"), BD.filename(orig_name)) - or T(_("Folder already exists:\n%1\nOverwrite folder?"), BD.directory(orig_name)), - ok_text = _("Overwrite"), - ok_callback = function() - doPaste() - end, + if skipped_nb > 0 then -- keep select mode on + local text = self.cutfile and T(N_("1 file was not moved", "%1 files were not moved", skipped_nb), skipped_nb) + or T(N_("1 file was not copied", "%1 files were not copied", skipped_nb), skipped_nb) + UIManager:show(InfoMessage:new{ + text = text, + icon = "notice-warning", }) else - doPaste() + self:onToggleSelectMode() end end @@ -961,11 +1013,18 @@ function FileManager:createFolder() input_dialog:onShowKeyboard() end -function FileManager:showDeleteFileDialog(file, post_delete_callback, pre_delete_callback) - local file_abs_path = BaseUtil.realpath(file) - local is_file = lfs.attributes(file_abs_path, "mode") == "file" +function FileManager:showDeleteFileDialog(filepath, post_delete_callback, pre_delete_callback) + local file = BaseUtil.realpath(filepath) + if file == nil then + UIManager:show(InfoMessage:new{ + text = T(_("File not found:\n%1"), BD.filepath(filepath)), + icon = "notice-warning", + }) + return + end + local is_file = isFile(file) local text = (is_file and _("Delete file permanently?") or _("Delete folder permanently?")) .. "\n\n" .. BD.filepath(file) - if is_file and DocSettings:hasSidecarFile(file_abs_path) then + if is_file and DocSettings:hasSidecarFile(file) then text = text .. "\n\n" .. _("Book settings, highlights and notes will be deleted.") end UIManager:show(ConfirmBox:new{ @@ -983,35 +1042,54 @@ function FileManager:showDeleteFileDialog(file, post_delete_callback, pre_delete end function FileManager:deleteFile(file, is_file) - local file_abs_path = BaseUtil.realpath(file) - if file_abs_path == nil then - UIManager:show(InfoMessage:new{ - text = T(_("File not found:\n%1"), BD.filepath(file)), - icon = "notice-warning", - }) - return - end - - local ok, err if is_file then - ok, err = os.remove(file_abs_path) - else - ok, err = BaseUtil.purgeDir(file_abs_path) - end - if ok and not err then - if is_file then - DocSettings.updateLocation(file) + local ok = os.remove(file) + if ok then + DocSettings.updateLocation(file) -- delete sdr ReadHistory:fileDeleted(file) - else - ReadHistory:folderDeleted(file) + ReadCollection:removeItem(file) + return true end - ReadCollection:removeItemByPath(file, not is_file) - return true else + local ok = BaseUtil.purgeDir(file) + if ok then + ReadHistory:folderDeleted(file) -- will delete sdr + ReadCollection:removeItemsByPath(file) + return true + end + end + UIManager:show(InfoMessage:new{ + text = T(_("Failed to delete:\n%1"), BD.filepath(file)), + icon = "notice-warning", + }) +end + +function FileManager:deleteSelectedFiles() + local ok_files = {} + for orig_file in pairs(self.selected_files) do + local file_abs_path = BaseUtil.realpath(orig_file) + local ok = file_abs_path and os.remove(file_abs_path) + if ok then + DocSettings.updateLocation(file_abs_path) -- delete sdr + ok_files[orig_file] = true + self.selected_files[orig_file] = nil + end + end + local skipped_nb = util.tableSize(self.selected_files) + if util.tableSize(ok_files) > 0 then + ReadHistory:removeItems(ok_files) + ReadCollection:removeItems(ok_files) + if skipped_nb > 0 then + self:onRefresh() + end + end + if skipped_nb > 0 then -- keep select mode on UIManager:show(InfoMessage:new{ - text = T(_("Failed to delete:\n%1"), BD.filepath(file)), + text = T(N_("Failed to delete 1 file.", "Failed to delete %1 files.", skipped_nb), skipped_nb), icon = "notice-warning", }) + else + self:onToggleSelectMode() end end @@ -1052,11 +1130,12 @@ function FileManager:renameFile(file, basename, is_file) if self:moveFile(file, dest) then if is_file then DocSettings.updateLocation(file, dest) - ReadHistory:updateItemByPath(file, dest) -- (will update "lastfile" if needed) + ReadHistory:updateItem(file, dest) -- (will update "lastfile" if needed) + ReadCollection:updateItem(file, dest) else ReadHistory:updateItemsByPath(file, dest) + ReadCollection:updateItemsByPath(file, dest) end - ReadCollection:updateItemByPath(file, dest) self:onRefresh() else UIManager:show(InfoMessage:new{ diff --git a/frontend/apps/filemanager/filemanagercollection.lua b/frontend/apps/filemanager/filemanagercollection.lua index 54d445643..82b196f81 100644 --- a/frontend/apps/filemanager/filemanagercollection.lua +++ b/frontend/apps/filemanager/filemanagercollection.lua @@ -10,7 +10,7 @@ local filemanagerutil = require("apps/filemanager/filemanagerutil") local _ = require("gettext") local FileManagerCollection = WidgetContainer:extend{ - coll_menu_title = _("Favorites"), + title = _("Favorites"), } function FileManagerCollection:init() @@ -19,28 +19,36 @@ end function FileManagerCollection:addToMainMenu(menu_items) menu_items.collections = { - text = self.coll_menu_title, + text = self.title, callback = function() - self:onShowColl("favorites") + self:onShowColl() end, } end function FileManagerCollection:updateItemTable() - -- Try to stay on current page. - local select_number = nil - if self.coll_menu.page and self.coll_menu.perpage then - select_number = (self.coll_menu.page - 1) * self.coll_menu.perpage + 1 + local item_table = {} + for _, item in pairs(ReadCollection.coll[self.coll_menu.collection_name]) do + table.insert(item_table, item) end - self.coll_menu:switchItemTable(self.coll_menu_title, - ReadCollection:prepareList(self.coll_menu.collection), select_number) + table.sort(item_table, function(v1, v2) return v1.order < v2.order end) + self.coll_menu:switchItemTable(self.title, item_table, -1) end function FileManagerCollection:onMenuChoice(item) - require("apps/reader/readerui"):showReader(item.file) + local file = item.file + if self.ui.document then + if self.ui.document.file ~= file then + self.ui:switchDocument(file) + end + else + local ReaderUI = require("apps/reader/readerui") + ReaderUI:showReader(file) + end end function FileManagerCollection:onMenuHold(item) + local file = item.file self.collfile_dialog = nil local function close_dialog_callback() UIManager:close(self.collfile_dialog) @@ -49,46 +57,45 @@ function FileManagerCollection:onMenuHold(item) UIManager:close(self.collfile_dialog) self._manager.coll_menu.close_callback() end - local function status_button_callback() + local function close_dialog_update_callback() UIManager:close(self.collfile_dialog) self._manager:updateItemTable() + self._manager.files_updated = true end - local is_currently_opened = item.file == (self.ui.document and self.ui.document.file) + local is_currently_opened = file == (self.ui.document and self.ui.document.file) local buttons = {} - if not item.dim then - local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or item.file - table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, status_button_callback)) - table.insert(buttons, {}) -- separator - end + local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file + table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback)) + table.insert(buttons, {}) -- separator table.insert(buttons, { - filemanagerutil.genResetSettingsButton(item.file, status_button_callback, is_currently_opened), + filemanagerutil.genResetSettingsButton(file, close_dialog_update_callback, is_currently_opened), { text = _("Remove from favorites"), callback = function() UIManager:close(self.collfile_dialog) - ReadCollection:removeItem(item.file, self._manager.coll_menu.collection) + ReadCollection:removeItem(file, self.collection_name) self._manager:updateItemTable() end, }, }) table.insert(buttons, { - filemanagerutil.genShowFolderButton(item.file, close_dialog_menu_callback, item.dim), - filemanagerutil.genBookInformationButton(item.file, close_dialog_callback, item.dim), + filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback), + filemanagerutil.genBookInformationButton(file, close_dialog_callback), }) table.insert(buttons, { - filemanagerutil.genBookCoverButton(item.file, close_dialog_callback, item.dim), - filemanagerutil.genBookDescriptionButton(item.file, close_dialog_callback, item.dim), + filemanagerutil.genBookCoverButton(file, close_dialog_callback), + filemanagerutil.genBookDescriptionButton(file, close_dialog_callback), }) - if Device:canExecuteScript(item.file) then + if Device:canExecuteScript(file) then table.insert(buttons, { - filemanagerutil.genExecuteScriptButton(item.file, close_dialog_menu_callback) + filemanagerutil.genExecuteScriptButton(file, close_dialog_menu_callback) }) end self.collfile_dialog = ButtonDialog:new{ - title = item.text:match("([^/]+)$"), + title = item.text, title_align = "center", buttons = buttons, } @@ -111,7 +118,8 @@ function FileManagerCollection:MenuSetRotationModeHandler(rotation) return true end -function FileManagerCollection:onShowColl(collection) +function FileManagerCollection:onShowColl(collection_name) + collection_name = collection_name or ReadCollection.default_collection_name self.coll_menu = Menu:new{ ui = self.ui, covers_fullscreen = true, -- hint for UIManager:_repaint() @@ -123,33 +131,31 @@ function FileManagerCollection:onShowColl(collection) onMenuHold = self.onMenuHold, onSetRotationMode = self.MenuSetRotationModeHandler, _manager = self, - collection = collection, + collection_name = collection_name, } - self:updateItemTable() self.coll_menu.close_callback = function() + if self.files_updated then + if self.ui.file_chooser then + self.ui.file_chooser:refreshPath() + end + self.files_updated = nil + end UIManager:close(self.coll_menu) + self.coll_menu = nil end + self:updateItemTable() UIManager:show(self.coll_menu) return true end function FileManagerCollection:showCollDialog() local coll_dialog - local is_added = self.ui.document and ReadCollection:checkItemExist(self.ui.document.file) local buttons = { {{ - text_func = function() - return is_added and _("Remove current book from favorites") or _("Add current book to favorites") - end, - enabled = self.ui.document and true or false, + text = _("Sort favorites"), callback = function() UIManager:close(coll_dialog) - if is_added then - ReadCollection:removeItem(self.ui.document.file) - else - ReadCollection:addItem(self.ui.document.file) - end - self:updateItemTable() + self:sortCollection() end, }}, {{ @@ -164,8 +170,8 @@ function FileManagerCollection:showCollDialog() return DocumentRegistry:hasProvider(file) end, onConfirm = function(file) - if not ReadCollection:checkItemExist(file) then - ReadCollection:addItem(file) + if not ReadCollection:hasFile(file) then + ReadCollection:addItem(file, self.coll_menu.collection_name) self:updateItemTable() end end, @@ -173,14 +179,24 @@ function FileManagerCollection:showCollDialog() UIManager:show(path_chooser) end, }}, - {{ - text = _("Sort favorites"), + } + if self.ui.document then + local has_file = ReadCollection:hasFile(self.ui.document.file) + table.insert(buttons, {{ + text_func = function() + return has_file and _("Remove current book from favorites") or _("Add current book to favorites") + end, callback = function() UIManager:close(coll_dialog) - self:sortCollection() + if has_file then + ReadCollection:removeItem(self.ui.document.file) + else + ReadCollection:addItem(self.ui.document.file, self.coll_menu.collection_name) + end + self:updateItemTable() end, - }}, - } + }}) + end coll_dialog = ButtonDialog:new{ buttons = buttons, } @@ -188,21 +204,14 @@ function FileManagerCollection:showCollDialog() end function FileManagerCollection:sortCollection() - local item_table = {} - for _, v in ipairs(self.coll_menu.item_table) do - table.insert(item_table, { text = v.text, label = v.file }) - end + local item_table = ReadCollection:getOrderedCollection(self.coll_menu.collection_name) local SortWidget = require("ui/widget/sortwidget") local sort_widget sort_widget = SortWidget:new{ title = _("Sort favorites"), item_table = item_table, callback = function() - local new_order_table = {} - for i, v in ipairs(sort_widget.item_table) do - table.insert(new_order_table, { file = v.label, order = i }) - end - ReadCollection:writeCollection(new_order_table, self.coll_menu.collection) + ReadCollection:updateCollectionOrder(self.coll_menu.collection_name, sort_widget.item_table) self:updateItemTable() end } diff --git a/frontend/apps/filemanager/filemanagershortcuts.lua b/frontend/apps/filemanager/filemanagershortcuts.lua index f996f50ce..49f7c6684 100644 --- a/frontend/apps/filemanager/filemanagershortcuts.lua +++ b/frontend/apps/filemanager/filemanagershortcuts.lua @@ -88,7 +88,7 @@ function FileManagerShortcuts:onMenuHold(item) text = _("Paste to folder"), callback = function() UIManager:close(dialog) - self._manager.ui:pasteHere(item.folder) + self._manager.ui:pasteFileFromClipboard(item.folder) end }, }) diff --git a/frontend/apps/filemanager/filemanagerutil.lua b/frontend/apps/filemanager/filemanagerutil.lua index a70fd127c..738acbfb8 100644 --- a/frontend/apps/filemanager/filemanagerutil.lua +++ b/frontend/apps/filemanager/filemanagerutil.lua @@ -224,15 +224,15 @@ end function filemanagerutil.genAddRemoveFavoritesButton(file, caller_callback, button_disabled) local ReadCollection = require("readcollection") - local is_added = ReadCollection:checkItemExist(file) + local has_file = ReadCollection:hasFile(file) return { text_func = function() - return is_added and _("Remove from favorites") or _("Add to favorites") + return has_file and _("Remove from favorites") or _("Add to favorites") end, enabled = not button_disabled, callback = function() caller_callback() - if is_added then + if has_file then ReadCollection:removeItem(file) else ReadCollection:addItem(file) diff --git a/frontend/readcollection.lua b/frontend/readcollection.lua index 7b235243e..5dd9ec185 100644 --- a/frontend/readcollection.lua +++ b/frontend/readcollection.lua @@ -2,149 +2,227 @@ local DataStorage = require("datastorage") local FFIUtil = require("ffi/util") local LuaSettings = require("luasettings") local lfs = require("libs/libkoreader-lfs") +local logger = require("logger") local util = require("util") -local DEFAULT_COLLECTION_NAME = "favorites" local collection_file = DataStorage:getSettingsDir() .. "/collection.lua" -local ReadCollection = {} +local ReadCollection = { + coll = {}, + last_read_time = 0, + default_collection_name = "favorites", +} -function ReadCollection:read(collection_name) - if not collection_name then collection_name = DEFAULT_COLLECTION_NAME end +local function buildEntry(file, order, mandatory) + file = FFIUtil.realpath(file) + if not file then return end + if not mandatory then -- new item + local attr = lfs.attributes(file) + if not attr or attr.mode ~= "file" then return end + mandatory = util.getFriendlySize(attr.size or 0) + end + return { + file = file, + text = file:gsub(".*/", ""), + mandatory = mandatory, + order = order, + } +end + +function ReadCollection:_read() + local collection_file_modification_time = lfs.attributes(collection_file, "modification") + if collection_file_modification_time then + if collection_file_modification_time <= self.last_read_time then return end + self.last_read_time = collection_file_modification_time + end local collections = LuaSettings:open(collection_file) - local coll = collections:readSetting(collection_name) or {} - local coll_max_item = 0 - for _, v in pairs(coll) do - if v.order > coll_max_item then - coll_max_item = v.order - end + if collections:hasNot(self.default_collection_name) then + collections:saveSetting(self.default_collection_name, {}) end - return coll, coll_max_item -end - -function ReadCollection:readAllCollection() - local collection = LuaSettings:open(collection_file) - if collection and collection.data then - return collection.data - else - return {} - end -end - -function ReadCollection:prepareList(collection_name) - local data = self:read(collection_name) - local list = {} - for _, v in pairs(data) do - local file_path = FFIUtil.realpath(v.file) or v.file -- keep orig file path of deleted files - local file_exists = lfs.attributes(file_path, "mode") == "file" - table.insert(list, { - order = v.order, - file = file_path, - text = v.file:gsub(".*/", ""), - dim = not file_exists, - mandatory = file_exists and util.getFriendlySize(lfs.attributes(file_path, "size") or 0) or "", - select_enabled = file_exists, - }) - end - table.sort(list, function(v1,v2) - return v1.order < v2.order - end) - return list -end - -function ReadCollection:removeItemByPath(path, is_dir) - local dir - local should_write = false - if is_dir then - path = path .. "/" - end - local coll = self:readAllCollection() - for i in pairs(coll) do - local single_collection = coll[i] - for item = #single_collection, 1, -1 do - if not is_dir and single_collection[item].file == path then - should_write = true - table.remove(single_collection, item) - elseif is_dir then - dir = util.splitFilePathName(single_collection[item].file) - if dir == path then - should_write = true - table.remove(single_collection, item) - end + logger.dbg("ReadCollection: reading from collection file") + self.coll = {} + for coll_name, collection in pairs(collections.data) do + local coll = {} + for _, v in ipairs(collection) do + local item = buildEntry(v.file, v.order) + if item then -- exclude deleted files + coll[item.file] = item end end + self.coll[coll_name] = coll end - if should_write then - local collection = LuaSettings:open(collection_file) - collection.data = coll - collection:flush() - end -end - -function ReadCollection:updateItemByPath(old_path, new_path) - local is_dir = false - local dir, file - if lfs.attributes(new_path, "mode") == "directory" then - is_dir = true - old_path = old_path .. "/" - end - local should_write = false - local coll = self:readAllCollection() - for i, j in pairs(coll) do - for k, v in pairs(j) do - if not is_dir and v.file == old_path then - should_write = true - coll[i][k].file = new_path - elseif is_dir then - dir, file = util.splitFilePathName(v.file) - if dir == old_path then - should_write = true - coll[i][k].file = string.format("%s/%s", new_path, file) - end +end + +function ReadCollection:write(collection_name) + local collections = LuaSettings:open(collection_file) + for coll_name, coll in pairs(self.coll) do + if not collection_name or coll_name == collection_name then + local data = {} + for _, item in pairs(coll) do + table.insert(data, { file = item.file, order = item.order }) end + collections:saveSetting(coll_name, data) end end - if should_write then - local collection = LuaSettings:open(collection_file) - collection.data = coll - collection:flush() + logger.dbg("ReadCollection: writing to collection file") + collections:flush() +end + +function ReadCollection:getFileCollectionName(file, collection_name) + file = FFIUtil.realpath(file) or file + for coll_name, coll in pairs(self.coll) do + if not collection_name or coll_name == collection_name then + if coll[file] then + return coll_name, file + end + end end end -function ReadCollection:removeItem(item, collection_name) - local coll = self:read(collection_name) - for k, v in pairs(coll) do - if v.file == item then - table.remove(coll, k) - break +function ReadCollection:hasFile(file, collection_name) + local coll_name = self:getFileCollectionName(file, collection_name) + return coll_name and true or false +end + +function ReadCollection:getCollectionMaxOrder(collection_name) + local max_order = 0 + for _, item in pairs(self.coll[collection_name]) do + if max_order < item.order then + max_order = item.order end end - self:writeCollection(coll, collection_name) + return max_order end -function ReadCollection:writeCollection(coll_items, collection_name) - local collection = LuaSettings:open(collection_file) - collection:saveSetting(collection_name or DEFAULT_COLLECTION_NAME, coll_items) - collection:flush() +function ReadCollection:getOrderedCollection(collection_name) + local ordered_coll = {} + for _, item in pairs(self.coll[collection_name]) do + table.insert(ordered_coll, item) + end + table.sort(ordered_coll, function(v1, v2) return v1.order < v2.order end) + return ordered_coll +end + +function ReadCollection:updateCollectionOrder(collection_name, ordered_coll) + local coll = self.coll[collection_name] + for i, item in ipairs(ordered_coll) do + coll[item.file].order = i + end + self:write(collection_name) end function ReadCollection:addItem(file, collection_name) - local coll, coll_max_item = self:read(collection_name) - local collection_item = { - file = file, - order = coll_max_item + 1, - } - table.insert(coll, collection_item) - self:writeCollection(coll, collection_name) + collection_name = collection_name or self.default_collection_name + local max_order = self:getCollectionMaxOrder(collection_name) + local item = buildEntry(file, max_order + 1) + self.coll[collection_name][item.file] = item + self:write(collection_name) end -function ReadCollection:checkItemExist(item, collection_name) - local coll = self:read(collection_name) - for _, v in pairs(coll) do - if v.file == item then - return true +function ReadCollection:addItems(files, collection_name) -- files = { filepath = true, } + collection_name = collection_name or self.default_collection_name + local coll = self.coll[collection_name] + local max_order = self:getCollectionMaxOrder(collection_name) + local do_write + for file in pairs(files) do + if not self:hasFile(file) then + max_order = max_order + 1 + local item = buildEntry(file, max_order) + coll[item.file] = item + do_write = true end end + if do_write then + self:write(collection_name) + end end +function ReadCollection:removeItem(file, collection_name, no_write) + local coll_name, file_name = self:getFileCollectionName(file, collection_name) + if coll_name then + self.coll[coll_name][file_name] = nil + if not no_write then + self:write(coll_name) + end + return true + end +end + +function ReadCollection:removeItems(files) -- files = { filepath = true, } + local do_write + for file in pairs(files) do + if self:removeItem(file, nil, true) then + do_write = true + end + end + if do_write then + self:write() + end +end + +function ReadCollection:removeItemsByPath(path) + local do_write + for coll_name, coll in pairs(self.coll) do + for file_name in pairs(coll) do + if util.stringStartsWith(file_name, path) then + self.coll[coll_name][file_name] = nil + do_write = true + end + end + end + if do_write then + self:write() + end +end + +function ReadCollection:_updateItem(coll_name, file_name, new_filepath, new_path) + local coll = self.coll[coll_name] + local item_old = coll[file_name] + local order, mandatory = item_old.order, item_old.mandatory + new_filepath = new_filepath or new_path .. "/" .. item_old.text + coll[file_name] = nil + local item = buildEntry(new_filepath, order, mandatory) -- no lfs call + coll[item.file] = item +end + +function ReadCollection:updateItem(file, new_filepath) + local coll_name, file_name = self:getFileCollectionName(file) + if coll_name then + self:_updateItem(coll_name, file_name, new_filepath) + self:write(coll_name) + end +end + +function ReadCollection:updateItems(files, new_path) -- files = { filepath = true, } + local do_write + for file in pairs(files) do + local coll_name, file_name = self:getFileCollectionName(file) + if coll_name then + self:_updateItem(coll_name, file_name, nil, new_path) + do_write = true + end + end + if do_write then + self:write() + end +end + +function ReadCollection:updateItemsByPath(path, new_path) + local len = #path + local do_write + for coll_name, coll in pairs(self.coll) do + for file_name in pairs(coll) do + if file_name:sub(1, len) == path then + self:_updateItem(coll_name, file_name, new_path .. file_name:sub(len + 1)) + do_write = true + end + end + end + if do_write then + self:write() + end +end + +ReadCollection:_read() + return ReadCollection diff --git a/frontend/readhistory.lua b/frontend/readhistory.lua index d33c8b17e..f01be5672 100644 --- a/frontend/readhistory.lua +++ b/frontend/readhistory.lua @@ -8,6 +8,7 @@ local util = require("util") local joinPath = ffiutil.joinPath local lfs = require("libs/libkoreader-lfs") local realpath = ffiutil.realpath +local C_ = require("gettext").pgettext local history_file = joinPath(DataStorage:getDataDir(), "history.lua") @@ -19,7 +20,7 @@ local ReadHistory = { local function getMandatory(date_time) return G_reader_settings:isTrue("history_datetime_short") - and datetime.secondsToDate(date_time):sub(3) or datetime.secondsToDateTime(date_time) + and os.date(C_("Date string", "%y-%m-%d"), date_time) or datetime.secondsToDateTime(date_time) end local function buildEntry(input_time, input_file) @@ -181,23 +182,38 @@ function ReadHistory:getFileByDirectory(directory, recursive) end --- Updates the history list after renaming/moving a file. -function ReadHistory:updateItemByPath(old_path, new_path) - local index = self:getIndexByFile(old_path) +function ReadHistory:updateItem(file, new_filepath) + local index = self:getIndexByFile(file) if index then - self.hist[index].file = new_path - self.hist[index].text = new_path:gsub(".*/", "") + local item = self.hist[index] + item.file = new_filepath + item.text = new_filepath:gsub(".*/", "") + self:_flush() + end +end + +function ReadHistory:updateItems(files, new_path) -- files = { filepath = true, } + local history_updated + for file in pairs(files) do + local index = self:getIndexByFile(file) + if index then + local item = self.hist[index] + item.file = new_path .. "/" .. item.text + history_updated = true + end + end + if history_updated then self:_flush() end end --- Updates the history list after renaming/moving a folder. function ReadHistory:updateItemsByPath(old_path, new_path) - old_path = "^"..old_path + local len = #old_path local history_updated for i, v in ipairs(self.hist) do - local file, count = v.file:gsub(old_path, new_path) - if count == 1 then - self.hist[i].file = file + if v.file:sub(1, len) == old_path then + self.hist[i].file = new_path .. v.file:sub(len + 1) history_updated = true end end @@ -248,6 +264,24 @@ function ReadHistory:folderDeleted(path) end end +function ReadHistory:removeItems(files) -- files = { filepath = true, } + local history_updated + for file in pairs(files) do + local index = self:getIndexByFile(file) + if index then + self:fileDeleted(index) + history_updated = true + end + end + if history_updated then + if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then + self:_flush() + else + self:ensureLastFile() + end + end +end + --- Removes the history item if the document settings has been reset. function ReadHistory:fileSettingsPurged(path) if G_reader_settings:isTrue("autoremove_deleted_items_from_history") then diff --git a/spec/unit/filemanager_spec.lua b/spec/unit/filemanager_spec.lua index ce46b6399..b55c0bd68 100644 --- a/spec/unit/filemanager_spec.lua +++ b/spec/unit/filemanager_spec.lua @@ -32,7 +32,7 @@ describe("FileManager module", function() assert.Equals(w.text, "File not found:\n"..tmp_fn) end assert.is_nil(lfs.attributes(tmp_fn)) - filemanager:deleteFile(tmp_fn, true) + filemanager:showDeleteFileDialog(tmp_fn) UIManager.show = old_show filemanager:onClose() end)