Add QR Code Encoder plugin (#226)
* init * feat: notify options; all, minimal and none * update: plugin.toml * update: add README * feat: add button toggle * feat: add configurable glyph * fix: valid image radius * fix: shell escape entry * feat: add settings * update: add thumbnail.webp * feat: add titlebar and titlebar toggle * feat: add custom image widget setting, reorder keys * fix: auto-size image with titlebar * update: README (new settings) * fix: lowercase correction_level label keys * fix: file type and move shellEscape to the cmd
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user