From 9917263e1682cacc1e582ab9a72f023449b70456 Mon Sep 17 00:00:00 2001 From: Slendi Date: Sat, 20 Sep 2025 14:25:21 +0300 Subject: [PATCH] Initial commit Signed-off-by: Slendi --- main.lua | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/main.lua b/main.lua index e69de29..4724d22 100644 --- a/main.lua +++ b/main.lua @@ -0,0 +1,265 @@ +--[[-- +KOReader plugin adding a "Upload to slenpaste" action to the File Manager long-press menu. + +@module koplugin.slenpaste +--]]-- + +local http = require("socket.http") +local ltn12 = require("ltn12") +local Device = require("device") +local InfoMessage = require("ui/widget/infomessage") +local QRMessage = require("ui/widget/qrmessage") +local RadioButtonWidget = require("ui/widget/radiobuttonwidget") +local ButtonDialog = require("ui/widget/buttondialog") +local Font = require("ui/font") +local TextBoxWidget = require("ui/widget/textboxwidget") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local util = require("util") +local _ = require("gettext") +local T = require("ffi/util").template + +local SlenPaste = WidgetContainer:extend{ + name = "slenpaste", + is_doc_only = false, + upload_url = "https://paste.slendi.dev/", + settings_key = "slenpaste", +} + +SlenPaste.expiry_options = { + { label = _("5 minutes"), value = "5m" }, + { label = _("1 hour"), value = "1h" }, + { label = _("1 day"), value = "1d" }, + { label = _("7 days"), value = "7d" }, + { label = _("30 days"), value = "30d" }, + { label = _("Expires on first view"), value = "view" }, + { label = _("Never"), value = "0" }, +} + +function SlenPaste:init() + self.settings = G_reader_settings:readSetting(self.settings_key, {}) + if not self.ui.document then + self:registerFileManagerActions() + end +end + +function SlenPaste:registerFileManagerActions() + local file_manager = self.ui + if not file_manager or not file_manager.addFileDialogButtons then + return + end + local plugin = self + file_manager:addFileDialogButtons("slenpaste_upload", function(file, is_file) + if not is_file then + return nil + end + return { + { + text = _("Upload to Slenpaste"), + callback = function() + if file_manager.file_dialog then + UIManager:close(file_manager.file_dialog) + end + plugin:promptExpiryAndUpload(file) + end, + }, + } + end) +end + +function SlenPaste:getDefaultExpiry() + local remembered = self.settings and self.settings.last_expiry + if remembered then + return remembered + end + return "1d" +end + +function SlenPaste:promptExpiryAndUpload(file_path) + local radio_buttons = {} + local default_expiry = self:getDefaultExpiry() + for index, option in ipairs(self.expiry_options) do + radio_buttons[index] = { + { + text = option.label, + provider = option.value, + checked = option.value == default_expiry, + }, + } + end + + UIManager:show(RadioButtonWidget:new{ + title_text = _("Slenpaste expiry"), + ok_text = _("Upload"), + cancel_text = _("Cancel"), + radio_buttons = radio_buttons, + callback = function(widget) + local expiry = widget.provider + self.settings.last_expiry = expiry + G_reader_settings:saveSetting(self.settings_key, self.settings) + self:startUpload(file_path, expiry) + end, + }) +end + +function SlenPaste:startUpload(file_path, expiry) + local uploading_message = InfoMessage:new{ + text = _("Uploading to slenpaste…"), + } + UIManager:show(uploading_message) + + local ok, result = self:performUpload(file_path, expiry) + + UIManager:close(uploading_message) + + if ok then + self:showSuccessDialog(result) + else + self:showError(result) + end +end + +function SlenPaste:performUpload(file_path, expiry) + local file, err = io.open(file_path, "rb") + if not file then + return false, err or _("Unable to open file") + end + + local filename = file_path:match("([^/]+)$") or "upload" + filename = filename:gsub('["\r\n]', "_") + + local boundary = "----KOReaderSlenpaste" .. tostring(os.time()) .. tostring(math.random(1000, 9999)) + local CRLF = "\r\n" + + local prefix = table.concat({ + "--", boundary, CRLF, + "Content-Disposition: form-data; name=\"expiry\"", CRLF, + CRLF, + tostring(expiry), CRLF, + "--", boundary, CRLF, + string.format("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"", filename), CRLF, + "Content-Type: application/octet-stream", CRLF, + CRLF, + }) + local suffix = CRLF .. "--" .. boundary .. "--" .. CRLF + + local file_size = file:seek("end") + file:seek("set", 0) + + local response_body = {} + local file_closed = false + local function close_file() + if not file_closed then + file_closed = true + file:close() + end + end + local function file_source() + if file_closed then + return nil + end + local chunk = file:read(8192) + if chunk then + return chunk + end + close_file() + return nil + end + + local request_body = ltn12.source.cat( + ltn12.source.string(prefix), + file_source, + ltn12.source.string(suffix) + ) + + local ok, status_code, headers, status_line = http.request{ + url = self.upload_url, + method = "POST", + headers = { + ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, + ["Content-Length"] = tostring(#prefix + file_size + #suffix), + }, + source = request_body, + sink = ltn12.sink.table(response_body), + } + + close_file() + + if not ok then + return false, status_code or _("HTTP request failed") + end + + local code = tonumber(status_code) or 0 + if code < 200 or code >= 300 then + local body_text = util.trim(table.concat(response_body)) + if body_text ~= "" then + return false, string.format("%s (%s)", status_line or "HTTP error", body_text) + end + return false, status_line or string.format("HTTP status %d", code) + end + + local url = util.trim(table.concat(response_body)) + if url == "" then + return false, _("Empty response from server") + end + + return true, url +end + +function SlenPaste:showSuccessDialog(url) + local buttons + local dialog + buttons = { + { + { + text = _("Copy link"), + enabled = Device:hasClipboard(), + callback = function() + Device.input.setClipboardText(url) + UIManager:show(InfoMessage:new{ + text = _("Link copied to clipboard"), + }) + end, + }, + { + text = _("Show QR code"), + callback = function() + UIManager:show(QRMessage:new{ + text = url, + width = Device.screen:getWidth(), + height = Device.screen:getHeight(), + }) + end, + }, + }, + { + { + text = _("Close"), + callback = function() + UIManager:close(dialog) + end, + }, + }, + } + + dialog = ButtonDialog:new{ + title = _("Slenpaste upload complete"), + buttons = buttons, + } + + dialog:addWidget(TextBoxWidget:new{ + text = url, + width = dialog:getAddedWidgetAvailableWidth(), + face = Font:getFace("infofont"), + }) + + UIManager:show(dialog) +end + +function SlenPaste:showError(message) + UIManager:show(InfoMessage:new{ + text = T(_("Upload failed:\n%s"), message), + }) +end + +return SlenPaste