diff --git a/qrcode/README.md b/qrcode/README.md new file mode 100644 index 00000000..d066eb06 --- /dev/null +++ b/qrcode/README.md @@ -0,0 +1,40 @@ +# QR Code encoder + +Transform any text or URL into a QR code completely offline. Then scan or copy the code. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `yocraft/qrcode` | +| Entries | Bar widget: `widget`; panel: `panel` | + +## Requirements + +Install `qrencode` on `PATH`. + +## Usage + +Open the panel from the bar widget or with this command: +```sh +noctalia msg panel-toggle yocraft/qrcode:panel +``` + +Enter your text or URL and click on Generate or press enter to generate the QR code, then scan it from the panel or copy it by clicking on it. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `titlebar` | `bool` | `true` | Show titlebar, panel name and buttons like settings and close. | +| `generate_button` | `bool` | `true` | Show Generate button, disable will submit on enter. | +| `close_on_copy` | `bool` | `false` | Close the panel when copying the QR code. | +| `notify` | `select` | `minimal` | Controls the notifications, minimal only notifies when Close on Copy is used. | +| `size` | `int` | `8` | Specify module size in dots (pixels). | +| `correction_level` | `select` | `M` | Specify error correction level. | +| `glyph` | `glyph` | `qrcode` | Bar widget icon glyph name. | +| `custom_image` | `image` | `""` | Path to a custom image; leave empty to use the icon glyph. | + +## Notes + +The plugin runs entirely locally and does not require network access. diff --git a/qrcode/panel.luau b/qrcode/panel.luau new file mode 100644 index 00000000..0e9e53c3 --- /dev/null +++ b/qrcode/panel.luau @@ -0,0 +1,226 @@ +local currentText = "" +local genCount = 0 +local errorMessage = nil +local imagePath = nil +local status = "idle" + +local imageSize = 400 + +local notifyConf = noctalia.getConfig("notify") + +local function notify(text, error) + local cmd = error and noctalia.notifyError or noctalia.notify + cmd("QR Code encoder", text) +end + +local function statusText() + if status == "generating" then + return noctalia.tr("panel.status.generating") + elseif status == "error" then + return noctalia.tr("panel.status.error", { message = errorMessage }) + elseif status == "ready" then + return noctalia.tr("panel.status.ready") + elseif status == "copied" then + return noctalia.tr("panel.status.copied") + else + return noctalia.tr("panel.status.idle") + end +end + +local function render() + local rowContent = { + ui.input({ + flexGrow = 1, + focus = true, + placeholder = noctalia.tr("panel.placeholder"), + onChange = "onTextChange", + onSubmit = "onTextSubmit", + }) + } + + if noctalia.getConfig("generate_button") then + table.insert( + rowContent, + ui.button({ + text = "Generate", + variant = "primary", + enabled = status ~= "generating", + onClick = "onGenerateClick", + }) + ) + end + + local content = { + ui.row({ gap = 8 }, rowContent), + ui.label({ + flexGrow = 1, + textAlign = "center", + text = statusText(), + color = status == "error" and "error" or "on_surface/0.75", + }) + } + + if (status == "ready" + or status == "copied" + or (status == "error" and errorMessage == noctalia.tr("panel.error.copy_failed"))) + and imagePath ~= nil then + table.insert( + content, + ui.row({ justify = "center" }, { + ui.image({ + path = imagePath, + width = imageSize, + height = imageSize, + radius = 24, + onClick = "onImageClick", + }) + }) + ) + end + + if noctalia.getConfig("titlebar") then + imageSize = 350 + table.insert( + content, 1, + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.label({ text = "QR Code Encoder", fontSize = 16, fontWeight = "bold", flexGrow = 1 }), + ui.button({ glyph = "settings", onClick = noctalia.openSettings }), + ui.button({ glyph = "close", onClick = "onCloseClicked" }), + }) + ) + end + + panel.render(ui.column({ flexGrow = 1, gap = 16 }, content)) +end + +local function shellEscape(raw) + return "'" .. raw:gsub("'", "'\\''") .. "'" +end + +local function generate(text) + if status == "generating" then + return + end + + text = noctalia.string.trim(text or "") + + if text == "" then + if imagePath ~= nil then + noctalia.removeFile(imagePath) + end + status = "idle" + errorMessage = nil + imagePath = nil + render() + return + end + + if not noctalia.commandExists("qrencode") then + status = "error" + errorMessage = noctalia.tr("panel.error.missing_qrencode") + + if notifyConf == "all" then + notify(statusText(), status == "error") + end + + render() + return + end + + status = "generating" + render() + + local dir = noctalia.pluginDir() + local outputPath = string.format("%s/qr-%s.png", dir, genCount) + local previousPath = imagePath + genCount += 1 + + local size = noctalia.getConfig("size") or 8 + local correctionLevel = noctalia.getConfig("correction_level") or "M" + + local cmd = string.format( + 'qrencode %s -o %s -s %s -l %s', + shellEscape(text), + outputPath, + size, + correctionLevel + ) + + noctalia.runAsync( + cmd, + function(result) + if result.exitCode == 0 and noctalia.fileExists(outputPath) then + imagePath = outputPath + status = "ready" + if previousPath ~= nil then + noctalia.removeFile(previousPath) + end + else + status = "error" + errorMessage = noctalia.tr("panel.error.generate_failed") + end + + if notifyConf == "all" then + notify(statusText(), status == "error") + end + + render() + end + ) +end + +function onOpen(_context) + status = "idle" + render() +end + +function onTextChange(value) + currentText = value +end + +function onTextSubmit(value) + currentText = value + generate(currentText) +end + +function onGenerateClick() + generate(currentText) +end + +function onImageClick() + local bytes, err = noctalia.readFile(imagePath) + errorMessage = noctalia.tr("panel.error.copy_failed") + + if bytes == nil then + status = "error" + else + local ok = noctalia.copyToClipboard(bytes, "image/png") + + status = ok and "copied" or "error" + end + + if notifyConf == "all" then + notify(statusText(), status == "error") + end + + if noctalia.getConfig("close_on_copy") then + if notifyConf == "minimal" then + notify(statusText(), status == "error") + end + + panel.close() + return + end + + render() +end + +function onCloseClicked() + panel.close() +end + +function onClose() + if imagePath ~= nil then + noctalia.removeFile(imagePath) + end +end diff --git a/qrcode/plugin.toml b/qrcode/plugin.toml new file mode 100644 index 00000000..00e7e8e4 --- /dev/null +++ b/qrcode/plugin.toml @@ -0,0 +1,93 @@ +id = "yocraft/qrcode" +name = "QR Code Encoder" +version = "1.0.0" +plugin_api = 15 +author = "yocraft" +license = "MIT" +deprecated = false +icon = "qrcode" +description = "Transform any text or URL in a QR code" +tags = [ "panel", "fun", "network", "privacy", "productivity", "utility" ] +dependencies = [ "qrencode" ] + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "widget.glyph.label" + description_key = "widget.glyph.description" + default = "qrcode" + + [[widget.setting]] + key = "custom_image" + type = "file" + label_key = "widget.custom_image.label" + description_key = "widget.custom_image.description" + default = "" + extensions = [".png", ".jpg", ".jpeg", ".webp", ".svg"] + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 500 +height = 550 +placement = "floating" +position = "center" + +[[setting]] +key = "titlebar" +type = "bool" +label_key = "settings.titlebar.label" +description_key = "settings.titlebar.description" +default = true + +[[setting]] +key = "generate_button" +type = "bool" +label_key = "settings.generate_button.label" +description_key = "settings.generate_button.description" +default = true + +[[setting]] +key = "close_on_copy" +type = "bool" +label_key = "settings.close_on_copy.label" +description_key = "settings.close_on_copy.description" +default = false + +[[setting]] +key = "notify" +type = "select" +label_key = "settings.notify.label" +description_key = "settings.notify.description" +default = "minimal" +options = [ + { value = "all", label_key = "settings.notify.all" }, + { value = "minimal", label_key = "settings.notify.minimal" }, + { value = "none", label_key = "settings.notify.none" }, +] + +[[setting]] +key = "size" +type = "int" +label_key = "settings.size.label" +description_key = "settings.size.description" +default = 8 +advanced = true + +[[setting]] +key = "correction_level" +type = "select" +label_key = "settings.correction_level.label" +description_key = "settings.correction_level.description" +default = "M" +options = [ + { value = "L", label_key = "settings.correction_level.l" }, + { value = "M", label_key = "settings.correction_level.m" }, + { value = "Q", label_key = "settings.correction_level.q" }, + { value = "H", label_key = "settings.correction_level.h" }, +] +advanced = true diff --git a/qrcode/thumbnail.webp b/qrcode/thumbnail.webp new file mode 100644 index 00000000..5219b82f Binary files /dev/null and b/qrcode/thumbnail.webp differ diff --git a/qrcode/translations/en.json b/qrcode/translations/en.json new file mode 100644 index 00000000..cd54f307 --- /dev/null +++ b/qrcode/translations/en.json @@ -0,0 +1,61 @@ +{ + "settings": { + "titlebar": { + "label": "Show Titlebar", + "description": "Show panel name and buttons like settings and close." + }, + "generate_button": { + "label": "Show Generate Button", + "description": "Disable will submit on enter." + }, + "close_on_copy": { + "label": "Close on Copy", + "description": "Close the panel when copying the QR code." + }, + "notify": { + "label": "Notify", + "description": "Controls the notifications, minimal only notifies when Close on Copy is used.", + "all": "All", + "minimal": "Minimal", + "none": "None" + }, + "size": { + "label": "QR Code Size", + "description": "Specify module size in dots (pixels)." + }, + "correction_level": { + "label": "Correction Level", + "description": "Specify error correction level.", + "l": "Lowest", + "m": "Medium", + "q": "High", + "h": "Highest" + } + }, + "widget": { + "tooltip": "Generate a QR Code", + "glyph": { + "label": "Glyph", + "description": "Icon glyph name" + }, + "custom_image": { + "label": "Custom image", + "description": "Path to a custom image; leave empty to use the icon glyph" + } + }, + "panel": { + "placeholder": "Enter text or url...", + "status": { + "generating": "Generating...", + "error": "Error: {message}", + "ready": "QR code ready. Scan it here or click it to copy.", + "copied": "QR code copied to clipboard.", + "idle": "Enter some text, then press Generate." + }, + "error": { + "generate_failed": "failed to generate the QR code.", + "missing_qrencode": "qrencode not installed.", + "copy_failed": "failed to copy the QR code." + } + } +} diff --git a/qrcode/widget.luau b/qrcode/widget.luau new file mode 100644 index 00000000..b7c9683c --- /dev/null +++ b/qrcode/widget.luau @@ -0,0 +1,13 @@ +local image = noctalia.getConfig("custom_image") + +if image ~= "" and image ~= nil then + barWidget.setImage(image) +else + barWidget.setGlyph(noctalia.getConfig("glyph")) +end + +barWidget.setTooltip(noctalia.tr("widget.tooltip")) + +function onClick() + noctalia.togglePanel("yocraft/qrcode:panel") +end