diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 894229f80..d2bef827b 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -302,6 +302,14 @@ function FileManager:setupLayout() end, }) end + if file_manager.archiveviewer and file_manager.archiveviewer:isSupported(file) then + table.insert(one_time_providers, { + provider_name = _("Archive viewer"), + callback = function() + file_manager.archiveviewer:openArchiveViewer(file) + end, + }) + end self:showSetProviderButtons(file, one_time_providers) end, }, diff --git a/plugins/archiveviewer.koplugin/_meta.lua b/plugins/archiveviewer.koplugin/_meta.lua new file mode 100644 index 000000000..f1dcae98a --- /dev/null +++ b/plugins/archiveviewer.koplugin/_meta.lua @@ -0,0 +1,6 @@ +local _ = require("gettext") +return { + name = "archiveviewer", + fullname = _("Archive viewer"), + description = _([[Enables exploring the content of zip archives.]]), +} diff --git a/plugins/archiveviewer.koplugin/main.lua b/plugins/archiveviewer.koplugin/main.lua new file mode 100644 index 000000000..a29761d40 --- /dev/null +++ b/plugins/archiveviewer.koplugin/main.lua @@ -0,0 +1,188 @@ +local BD = require("ui/bidi") +local CenterContainer = require("ui/widget/container/centercontainer") +local ConfirmBox = require("ui/widget/confirmbox") +local Menu = require("ui/widget/menu") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local ffiUtil = require("ffi/util") +local util = require("util") +local _ = require("gettext") +local Screen = require("device").screen +local T = ffiUtil.template + +local ArchiveViewer = WidgetContainer:extend{ + -- list_table is a flat table containing archive files and folders + -- key - a full path of the folder ("/" for root), for all folders and subfolders of any level + -- value - a subtable of subfolders and files in the folder + -- subtable key - a name of a subfolder ending with /, or a name of a file (without path) + -- subtable value - false for subfolders, or file size (string) + list_table = nil, + arc_type = nil, + arc_ext = { + cbz = true, + epub = true, + zip = true, + }, +} + +function ArchiveViewer:isSupported(file) + return self.arc_ext[util.getFileNameSuffix(file):lower()] and true or false +end + +function ArchiveViewer:openArchiveViewer(file) + local _, filename = util.splitFilePathName(file) + local fileext = util.getFileNameSuffix(file):lower() + if fileext == "cbz" or fileext == "epub" or fileext == "zip" then + self.arc_type = "zip" + end + + self.fm_updated = nil + self.list_table = {} + if self.arc_type == "zip" then + self:getZipListTable(file) + else -- add other archivers here + return + end + + local item_table = self:getItemTable() -- root + local menu_container = CenterContainer:new{ + dimen = Screen:getSize(), + } + local menu = Menu:new{ + is_borderless = true, + is_popout = false, + title_multilines = true, + show_parent = menu_container, + onMenuSelect = function(self_menu, item) + if item.text:sub(-1) == "/" then -- folder + local title = item.path == "" and filename or filename.."/"..item.path + self_menu:switchItemTable(title, self:getItemTable(item.path)) + else + self:extractFile(file, item.path) + end + end, + close_callback = function() + UIManager:close(menu_container) + if self.fm_updated then + self.ui:onRefresh() + end + end, + } + table.insert(menu_container, menu) + menu:switchItemTable(filename, item_table) + UIManager:show(menu_container) +end + +function ArchiveViewer:getZipListTable(file) + local function parse_path(filepath, filesize) + if not filepath then return end + local path, name = util.splitFilePathName(filepath) + if path == "" then + path = "/" + end + if not self.list_table[path] then + self.list_table[path] = {} + end + if name == "" then -- some archivers include subfolder name as a separate entry ending with "/" + if path ~= "/" then + parse_path(path:sub(1,-2), false) -- filesize == false for subfolders + end + else + if self.list_table[path][name] == nil then + self.list_table[path][name] = filesize + parse_path(path:sub(1,-2), false) -- go up to include subfolders of the branch into the table + end + end + end + + local std_out = io.popen("unzip ".."-qql \""..file.."\"") + for line in std_out:lines() do + -- entry datetime not used so far + local fsize, fname = string.match(line, "%s+(%d+)%s+%d%d%-%d%d%-%d%d%d%d%s+%d%d:%d%d%s+(.+)") + parse_path(fname, fsize or 0) + end +end + +function ArchiveViewer:getItemTable(path) + local prefix, item_table + if path == nil or path == "" then -- root + path = "/" + prefix = "" + item_table = {} + else + prefix = path + item_table = { + { + text = BD.mirroredUILayout() and BD.ltr("../ ⬆") or "⬆ ../", + path = util.splitFilePathName(path:sub(1,-2)), + }, + } + end + + local files, dirs = {}, {} + for name, v in pairs(self.list_table[path] or {}) do + if v then -- file + table.insert(files, { + text = name, + bidi_wrap_func = BD.filename, + path = prefix..name, + mandatory = util.getFriendlySize(tonumber(v)), + }) + else -- folder + local dirname = name.."/" + table.insert(dirs, { + text = dirname, + bidi_wrap_func = BD.directory, + path = prefix..dirname, + mandatory = self:getItemDirMandatory(prefix..dirname), + }) + end + end + local sorting = function(a, b) -- by name, folders first + return ffiUtil.strcoll(a.text, b.text) + end + table.sort(dirs, sorting) + table.sort(files, sorting) + for _, v in ipairs(dirs) do + table.insert(item_table, v) + end + for _, v in ipairs(files) do + table.insert(item_table, v) + end + return item_table +end + +function ArchiveViewer:getItemDirMandatory(name) + local sub_dirs, dir_files = 0, 0 + for _, v in pairs(self.list_table[name]) do + if v then + dir_files = dir_files + 1 + else + sub_dirs = sub_dirs + 1 + end + end + local text = T("%1 \u{F016}", dir_files) + if sub_dirs > 0 then + text = T("%1 \u{F114} ", sub_dirs) .. text + end + return text +end + +function ArchiveViewer:extractFile(arcfile, filepath) + UIManager:show(ConfirmBox:new{ + text = _("Extract this file?") .. "\n\n" .. filepath .. "\n\n" .. + _("If the file already exists, it will be overwritten."), + ok_text = _("Extract"), + ok_callback = function() + if self.arc_type == "zip" then + io.popen("unzip ".."-qo \""..arcfile.."\"".." ".."\""..filepath.."\"".. + " -d ".."\""..util.splitFilePathName(arcfile).."\"") + else -- add other archivers here + return + end + self.fm_updated = true + end, + }) +end + +return ArchiveViewer