Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions qrcode/README.md
Original file line number Diff line number Diff line change
@@ -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.
226 changes: 226 additions & 0 deletions qrcode/panel.luau
Original file line number Diff line number Diff line change
@@ -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
93 changes: 93 additions & 0 deletions qrcode/plugin.toml
Original file line number Diff line number Diff line change
@@ -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 added qrcode/thumbnail.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading