Add davemhammer/gocryptfs (#324)
Mount, unmount, init, and auto-mount gocryptfs volumes from Noctalia. Passwords use secret-tool (desktop keyring) plus keyctl session cache.
This commit is contained in:
@@ -0,0 +1,878 @@
|
||||
--!nonstrict
|
||||
-- Gocryptfs manager panel: list, mount/unmount, add/edit, init, password prompt.
|
||||
|
||||
local STATE_KEY = "gocrypt_snapshot"
|
||||
local COMMAND_KEY = "gocrypt_command"
|
||||
local RESULT_KEY = "gocrypt_action_result"
|
||||
|
||||
local snapshot = noctalia.state.get(STATE_KEY) or {
|
||||
available = false,
|
||||
loading = true,
|
||||
busy = false,
|
||||
volumes = {},
|
||||
mountedCount = 0,
|
||||
totalCount = 0,
|
||||
error = "",
|
||||
updatedAt = 0,
|
||||
revision = 0,
|
||||
}
|
||||
|
||||
local selectedId = ""
|
||||
local requestCounter = 0
|
||||
local feedback = ""
|
||||
local feedbackError = false
|
||||
local dirty = true
|
||||
|
||||
-- views: "list" | "form" | "password"
|
||||
local view = "list"
|
||||
local formMode = "add" -- add | edit | init
|
||||
local formGeneration = 0
|
||||
local formName = ""
|
||||
local formCipher = ""
|
||||
local formMount = ""
|
||||
local formPassfile = ""
|
||||
local formAllowOther = false
|
||||
local formReadOnly = false
|
||||
local formAutoMount = false
|
||||
local formPlaintextNames = false
|
||||
local formAesSiv = false
|
||||
local formSavePassfile = true
|
||||
local formPassword = ""
|
||||
local formPasswordConfirm = ""
|
||||
local formPasswordKey = 0
|
||||
local formError = ""
|
||||
local formEditId = ""
|
||||
|
||||
local passwordVolumeId = ""
|
||||
local passwordVolumeName = ""
|
||||
local passwordValue = ""
|
||||
local passwordKey = 0
|
||||
local passwordError = ""
|
||||
-- "mount" | "store_keyring"
|
||||
local passwordMode = "mount"
|
||||
local passwordRememberKeyring = true
|
||||
|
||||
local render
|
||||
|
||||
local function tr(key, subst)
|
||||
return noctalia.tr(key, subst)
|
||||
end
|
||||
|
||||
local function nextRequestId()
|
||||
requestCounter += 1
|
||||
return `panel-{requestCounter}`
|
||||
end
|
||||
|
||||
local function sendCommand(action, values)
|
||||
local command = {
|
||||
action = action,
|
||||
requestId = nextRequestId(),
|
||||
}
|
||||
if type(values) == "table" then
|
||||
for key, value in pairs(values) do
|
||||
command[key] = value
|
||||
end
|
||||
end
|
||||
noctalia.state.set(COMMAND_KEY, command)
|
||||
return command.requestId
|
||||
end
|
||||
|
||||
local function selectedVolume()
|
||||
if selectedId == "" then
|
||||
return nil
|
||||
end
|
||||
for _, vol in ipairs(snapshot.volumes or {}) do
|
||||
if vol.id == selectedId then
|
||||
return vol
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function resetForm()
|
||||
formName = ""
|
||||
formCipher = ""
|
||||
formMount = ""
|
||||
formPassfile = ""
|
||||
formAllowOther = false
|
||||
formReadOnly = false
|
||||
formAutoMount = true
|
||||
formPlaintextNames = false
|
||||
formAesSiv = false
|
||||
formSavePassfile = true
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
formPasswordKey += 1
|
||||
formError = ""
|
||||
formEditId = ""
|
||||
formGeneration += 1
|
||||
end
|
||||
|
||||
local function fillFormFrom(vol)
|
||||
formName = tostring(vol.name or "")
|
||||
formCipher = tostring(vol.cipherDir or "")
|
||||
formMount = tostring(vol.mountPoint or "")
|
||||
formPassfile = tostring(vol.passfile or "")
|
||||
formAllowOther = vol.allowOther == true
|
||||
formReadOnly = vol.readOnly == true
|
||||
formAutoMount = vol.autoMount == true
|
||||
formPlaintextNames = false
|
||||
formAesSiv = false
|
||||
formSavePassfile = false
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
formPasswordKey += 1
|
||||
formError = ""
|
||||
formEditId = tostring(vol.id or "")
|
||||
formGeneration += 1
|
||||
end
|
||||
|
||||
local function statusColor(vol)
|
||||
if vol.mounted then
|
||||
return "tertiary"
|
||||
end
|
||||
if vol.cipherExists == false or vol.initialized == false then
|
||||
return "error"
|
||||
end
|
||||
return "on_surface_variant"
|
||||
end
|
||||
|
||||
local function volumeCard(vol)
|
||||
local selected = vol.id == selectedId
|
||||
local statusText = vol.mounted and tr("panel.status.mounted") or tr("panel.status.unmounted")
|
||||
local summary = `{vol.name} · {statusText} · {vol.mountPoint}`
|
||||
|
||||
return ui.button({
|
||||
key = vol.id,
|
||||
text = summary,
|
||||
glyph = vol.mounted and "lock-open" or "lock",
|
||||
contentAlign = "start",
|
||||
variant = selected and "primary" or "outline",
|
||||
selected = selected,
|
||||
onClick = function()
|
||||
selectedId = vol.id
|
||||
feedback = ""
|
||||
render()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function selectionToolbar()
|
||||
local vol = selectedVolume()
|
||||
if vol == nil then
|
||||
return ui.label({ text = tr("panel.select_hint"), color = "on_surface_variant" })
|
||||
end
|
||||
|
||||
local mounted = vol.mounted == true
|
||||
local busy = snapshot.busy == true
|
||||
local hasPassfile = type(vol.passfile) == "string" and vol.passfile ~= ""
|
||||
local useKeyring = vol.useKeyring == true
|
||||
local hints = {}
|
||||
if useKeyring then
|
||||
table.insert(hints, tr("panel.keyring_hint"))
|
||||
elseif hasPassfile then
|
||||
table.insert(hints, tr("panel.passfile_hint"))
|
||||
end
|
||||
if vol.autoMount == true and (useKeyring or hasPassfile) then
|
||||
table.insert(hints, tr("panel.automount_hint"))
|
||||
end
|
||||
|
||||
local buttons = {
|
||||
ui.button({
|
||||
text = mounted and tr("actions.unmount") or tr("actions.mount"),
|
||||
glyph = mounted and "lock" or "lock-open",
|
||||
variant = "primary",
|
||||
enabled = not busy and snapshot.available == true,
|
||||
onClick = "onToggleMount",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.open"),
|
||||
glyph = "folder-open",
|
||||
variant = "outline",
|
||||
enabled = not busy,
|
||||
onClick = "onOpenMount",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.edit"),
|
||||
glyph = "edit",
|
||||
variant = "outline",
|
||||
enabled = not busy and not mounted,
|
||||
onClick = "onEdit",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.remove"),
|
||||
glyph = "trash",
|
||||
variant = "destructive",
|
||||
enabled = not busy and not mounted,
|
||||
onClick = "onRemove",
|
||||
}),
|
||||
}
|
||||
|
||||
return ui.column({ gap = 4, padding = 10, fill = "surface_variant/0.45", radius = 10 }, {
|
||||
ui.row({ gap = 8, align = "center" }, {
|
||||
ui.glyph({ name = mounted and "lock-open" or "lock", size = 18, color = statusColor(vol) }),
|
||||
ui.label({ text = tostring(vol.name), fontWeight = "bold", flexGrow = 1, maxLines = 1 }),
|
||||
ui.label({
|
||||
text = mounted and tr("panel.status.mounted") or tr("panel.status.unmounted"),
|
||||
color = statusColor(vol),
|
||||
fontSize = 12,
|
||||
}),
|
||||
}),
|
||||
ui.label({
|
||||
text = tr("panel.cipher", { path = vol.cipherDir }),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 12,
|
||||
maxLines = 1,
|
||||
}),
|
||||
ui.label({
|
||||
text = tr("panel.mountpoint", { path = vol.mountPoint }),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 12,
|
||||
maxLines = 1,
|
||||
}),
|
||||
ui.label({
|
||||
text = table.concat(hints, " · "),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 11,
|
||||
visible = #hints > 0,
|
||||
}),
|
||||
ui.row({ gap = 6, align = "center" }, buttons),
|
||||
})
|
||||
end
|
||||
|
||||
local function volumeList()
|
||||
local vols = snapshot.volumes or {}
|
||||
if #vols == 0 then
|
||||
return ui.column({ align = "center", padding = 24, gap = 8 }, {
|
||||
ui.glyph({ name = "lock", size = 42, color = "on_surface_variant" }),
|
||||
ui.label({ text = tr("panel.empty"), color = "on_surface_variant", textAlign = "center" }),
|
||||
})
|
||||
end
|
||||
local rows = {}
|
||||
for _, vol in ipairs(vols) do
|
||||
table.insert(rows, volumeCard(vol))
|
||||
end
|
||||
return ui.column({ gap = 8 }, rows)
|
||||
end
|
||||
|
||||
local function formTitle()
|
||||
if formMode == "edit" then
|
||||
return tr("panel.edit_title")
|
||||
end
|
||||
if formMode == "init" then
|
||||
return tr("panel.init_title")
|
||||
end
|
||||
return tr("panel.add_title")
|
||||
end
|
||||
|
||||
local function formView()
|
||||
local isInit = formMode == "init"
|
||||
local children = {
|
||||
ui.row({ gap = 8, align = "center" }, {
|
||||
ui.label({ text = formTitle(), fontSize = 16, fontWeight = "bold", flexGrow = 1 }),
|
||||
ui.button({ glyph = "close", onClick = "onCancelForm" }),
|
||||
}),
|
||||
ui.label({
|
||||
text = isInit and tr("panel.init_help") or "",
|
||||
color = "on_surface_variant",
|
||||
fontSize = 12,
|
||||
visible = isInit,
|
||||
}),
|
||||
ui.label({ text = tr("panel.field.name"), color = "on_surface_variant" }),
|
||||
ui.input({
|
||||
key = `form-name-{formGeneration}`,
|
||||
value = formName,
|
||||
placeholder = tr("panel.field.name_placeholder"),
|
||||
onChange = "onFormName",
|
||||
}),
|
||||
ui.label({ text = tr("panel.field.cipher"), color = "on_surface_variant" }),
|
||||
ui.input({
|
||||
key = `form-cipher-{formGeneration}`,
|
||||
value = formCipher,
|
||||
placeholder = tr("panel.field.cipher_placeholder"),
|
||||
onChange = "onFormCipher",
|
||||
}),
|
||||
ui.label({ text = tr("panel.field.mount"), color = "on_surface_variant" }),
|
||||
ui.input({
|
||||
key = `form-mount-{formGeneration}`,
|
||||
value = formMount,
|
||||
placeholder = tr("panel.field.mount_placeholder"),
|
||||
onChange = "onFormMount",
|
||||
}),
|
||||
}
|
||||
|
||||
if isInit then
|
||||
table.insert(children, ui.label({ text = tr("panel.field.password"), color = "on_surface_variant" }))
|
||||
table.insert(children, ui.input({
|
||||
key = `form-pw-{formGeneration}-{formPasswordKey}`,
|
||||
value = "",
|
||||
placeholder = tr("panel.password_placeholder"),
|
||||
password = true,
|
||||
onChange = "onFormPassword",
|
||||
}))
|
||||
table.insert(children, ui.label({ text = tr("panel.field.password_confirm"), color = "on_surface_variant" }))
|
||||
table.insert(children, ui.input({
|
||||
key = `form-pw2-{formGeneration}-{formPasswordKey}`,
|
||||
value = "",
|
||||
placeholder = tr("panel.field.password_confirm_placeholder"),
|
||||
password = true,
|
||||
onChange = "onFormPasswordConfirm",
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formPlaintextNames, onChange = "onFormPlaintextNames" }),
|
||||
ui.label({ text = tr("panel.field.plaintextnames"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formAesSiv, onChange = "onFormAesSiv" }),
|
||||
ui.label({ text = tr("panel.field.aessiv"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formSavePassfile, onChange = "onFormSavePassfile" }),
|
||||
ui.label({ text = tr("panel.field.save_passfile"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center", visible = formSavePassfile }, {
|
||||
ui.toggle({ checked = formAutoMount, onChange = "onFormAutoMount" }),
|
||||
ui.label({ text = tr("panel.field.auto_mount"), flexGrow = 1 }),
|
||||
}))
|
||||
else
|
||||
-- Edit existing volume: keyring remember/forget + advanced passfile
|
||||
local editVol = selectedVolume()
|
||||
local editHasKeyring = editVol and editVol.useKeyring == true
|
||||
table.insert(children, ui.label({
|
||||
text = tr("panel.field.keyring_section"),
|
||||
color = "on_surface_variant",
|
||||
fontWeight = "bold",
|
||||
}))
|
||||
table.insert(children, ui.label({
|
||||
text = editHasKeyring and tr("panel.keyring_hint") or tr("panel.keyring_not_set"),
|
||||
color = editHasKeyring and "tertiary" or "on_surface_variant",
|
||||
fontSize = 12,
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 8, align = "center" }, {
|
||||
ui.button({
|
||||
text = tr("actions.remember"),
|
||||
glyph = "key",
|
||||
variant = "outline",
|
||||
enabled = snapshot.busy ~= true,
|
||||
onClick = "onRememberKeyring",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.forget"),
|
||||
glyph = "key-off",
|
||||
variant = "destructive",
|
||||
enabled = snapshot.busy ~= true and editHasKeyring == true,
|
||||
onClick = "onForgetKeyring",
|
||||
}),
|
||||
}))
|
||||
table.insert(children, ui.label({
|
||||
text = tr("panel.keyring_help"),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 11,
|
||||
maxLines = 4,
|
||||
}))
|
||||
table.insert(children, ui.label({ text = tr("panel.field.passfile"), color = "on_surface_variant" }))
|
||||
table.insert(children, ui.input({
|
||||
key = `form-passfile-{formGeneration}`,
|
||||
value = formPassfile,
|
||||
placeholder = tr("panel.field.passfile_placeholder"),
|
||||
onChange = "onFormPassfile",
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formAutoMount, onChange = "onFormAutoMount" }),
|
||||
ui.label({ text = tr("panel.field.auto_mount"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.label({
|
||||
text = tr("panel.field.auto_mount_help"),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 11,
|
||||
}))
|
||||
end
|
||||
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formAllowOther, onChange = "onFormAllowOther" }),
|
||||
ui.label({ text = tr("panel.field.allow_other"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = formReadOnly, onChange = "onFormReadOnly" }),
|
||||
ui.label({ text = tr("panel.field.read_only"), flexGrow = 1 }),
|
||||
}))
|
||||
table.insert(children, ui.label({ text = formError, color = "error", visible = formError ~= "" }))
|
||||
table.insert(children, ui.row({ justify = "end", gap = 8 }, {
|
||||
ui.button({ text = tr("actions.cancel"), variant = "outline", onClick = "onCancelForm" }),
|
||||
ui.button({
|
||||
text = isInit and tr("actions.init") or tr("actions.save"),
|
||||
glyph = isInit and "shield-lock" or "check",
|
||||
variant = "primary",
|
||||
enabled = snapshot.busy ~= true,
|
||||
onClick = "onSaveForm",
|
||||
}),
|
||||
}))
|
||||
|
||||
return ui.scroll({ flexGrow = 1, gap = 10 }, children)
|
||||
end
|
||||
|
||||
local function passwordView()
|
||||
local isStore = passwordMode == "store_keyring"
|
||||
local title = isStore
|
||||
and tr("panel.keyring_title", { name = passwordVolumeName })
|
||||
or tr("panel.password_title", { name = passwordVolumeName })
|
||||
local confirmText = isStore and tr("actions.remember") or tr("actions.mount")
|
||||
local confirmGlyph = isStore and "key" or "lock-open"
|
||||
local children = {
|
||||
ui.row({ gap = 8, align = "center" }, {
|
||||
ui.label({
|
||||
text = title,
|
||||
fontSize = 16,
|
||||
fontWeight = "bold",
|
||||
flexGrow = 1,
|
||||
}),
|
||||
ui.button({ glyph = "close", onClick = "onCancelPassword" }),
|
||||
}),
|
||||
ui.label({
|
||||
text = isStore and tr("panel.keyring_label") or tr("panel.password_label"),
|
||||
color = "on_surface_variant",
|
||||
}),
|
||||
ui.input({
|
||||
key = "pw_" .. tostring(passwordKey),
|
||||
value = "",
|
||||
placeholder = tr("panel.password_placeholder"),
|
||||
password = true,
|
||||
focus = true,
|
||||
onChange = "onPasswordChange",
|
||||
onSubmit = "onConfirmPassword",
|
||||
}),
|
||||
}
|
||||
if not isStore then
|
||||
table.insert(children, ui.row({ gap = 10, align = "center" }, {
|
||||
ui.toggle({ checked = passwordRememberKeyring, onChange = "onPasswordRemember" }),
|
||||
ui.label({ text = tr("panel.remember_keyring"), flexGrow = 1 }),
|
||||
}))
|
||||
else
|
||||
table.insert(children, ui.label({
|
||||
text = tr("panel.keyring_help"),
|
||||
color = "on_surface_variant",
|
||||
fontSize = 11,
|
||||
maxLines = 4,
|
||||
}))
|
||||
end
|
||||
table.insert(children, ui.label({ text = passwordError, color = "error", visible = passwordError ~= "" }))
|
||||
table.insert(children, ui.row({ justify = "end", gap = 8 }, {
|
||||
ui.button({ text = tr("actions.cancel"), variant = "outline", onClick = "onCancelPassword" }),
|
||||
ui.button({
|
||||
text = confirmText,
|
||||
glyph = confirmGlyph,
|
||||
variant = "primary",
|
||||
enabled = snapshot.busy ~= true,
|
||||
onClick = "onConfirmPassword",
|
||||
}),
|
||||
}))
|
||||
return ui.column({ flexGrow = 1, gap = 12 }, children)
|
||||
end
|
||||
|
||||
local function listView()
|
||||
local statusRows = {}
|
||||
if snapshot.loading == true then
|
||||
table.insert(statusRows, ui.label({ text = tr("panel.loading"), color = "on_surface_variant" }))
|
||||
end
|
||||
if snapshot.busy == true then
|
||||
table.insert(statusRows, ui.label({ text = tr("panel.busy"), color = "primary" }))
|
||||
end
|
||||
if type(snapshot.error) == "string" and snapshot.error ~= "" then
|
||||
table.insert(statusRows, ui.label({ text = snapshot.error, color = "error", maxLines = 2 }))
|
||||
end
|
||||
if feedback ~= "" then
|
||||
table.insert(statusRows, ui.label({
|
||||
text = feedback,
|
||||
color = feedbackError and "error" or "tertiary",
|
||||
maxLines = 2,
|
||||
}))
|
||||
end
|
||||
|
||||
return ui.column({ flexGrow = 1, gap = 10 }, {
|
||||
selectionToolbar(),
|
||||
ui.column({ gap = 3 }, statusRows),
|
||||
ui.scroll({ flexGrow = 1, gap = 8 }, { volumeList() }),
|
||||
})
|
||||
end
|
||||
|
||||
render = function()
|
||||
dirty = false
|
||||
|
||||
local content
|
||||
if view == "form" then
|
||||
content = formView()
|
||||
elseif view == "password" then
|
||||
content = passwordView()
|
||||
else
|
||||
content = listView()
|
||||
end
|
||||
|
||||
panel.render(ui.column({ flexGrow = 1, gap = 10 }, {
|
||||
ui.row({ align = "center", gap = 8 }, {
|
||||
ui.glyph({
|
||||
name = "lock",
|
||||
size = 24,
|
||||
color = snapshot.available and "primary" or "on_surface_variant",
|
||||
}),
|
||||
ui.column({ flexGrow = 1, gap = 0 }, {
|
||||
ui.label({ text = tr("title"), fontSize = 18, fontWeight = "bold" }),
|
||||
ui.label({
|
||||
text = tr("panel.subtitle"),
|
||||
fontSize = 11,
|
||||
color = "on_surface_variant",
|
||||
}),
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.init"),
|
||||
glyph = "shield-lock",
|
||||
variant = "outline",
|
||||
visible = view == "list",
|
||||
onClick = "onInit",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.add"),
|
||||
glyph = "plus",
|
||||
variant = "outline",
|
||||
visible = view == "list",
|
||||
onClick = "onAdd",
|
||||
}),
|
||||
ui.button({
|
||||
text = tr("actions.refresh"),
|
||||
glyph = "refresh",
|
||||
variant = "outline",
|
||||
visible = view == "list",
|
||||
onClick = "onRefresh",
|
||||
}),
|
||||
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
|
||||
}),
|
||||
content,
|
||||
ui.label({
|
||||
text = (snapshot.updatedAt or 0) > 0
|
||||
and tr("panel.updated", { time = noctalia.formatTime("%H:%M:%S", snapshot.updatedAt) })
|
||||
or "",
|
||||
color = "on_surface_variant",
|
||||
fontSize = 11,
|
||||
visible = view == "list",
|
||||
}),
|
||||
}))
|
||||
end
|
||||
|
||||
noctalia.state.watch(STATE_KEY, function(value)
|
||||
if type(value) ~= "table" then
|
||||
return
|
||||
end
|
||||
-- Always re-render on snapshot publish. Mounted flags live inside
|
||||
-- volumes[] and must update the list immediately after mount/unmount.
|
||||
snapshot = value
|
||||
if selectedId ~= "" and selectedVolume() == nil then
|
||||
selectedId = ""
|
||||
end
|
||||
dirty = true
|
||||
end)
|
||||
|
||||
noctalia.state.watch(RESULT_KEY, function(result)
|
||||
if type(result) ~= "table" then
|
||||
return
|
||||
end
|
||||
if type(result.requestId) ~= "string" or not result.requestId:match("^panel%-") then
|
||||
return
|
||||
end
|
||||
feedback = tostring(result.message or "")
|
||||
feedbackError = result.ok ~= true
|
||||
if result.ok == true and (
|
||||
result.action == "add_volume"
|
||||
or result.action == "update_volume"
|
||||
or result.action == "init_volume"
|
||||
) then
|
||||
view = "list"
|
||||
resetForm()
|
||||
end
|
||||
if result.ok == true and result.action == "mount" then
|
||||
view = "list"
|
||||
passwordValue = ""
|
||||
passwordError = ""
|
||||
end
|
||||
if result.ok ~= true and result.action == "mount" and view == "password" then
|
||||
passwordError = feedback
|
||||
passwordKey += 1
|
||||
passwordValue = ""
|
||||
end
|
||||
if result.ok ~= true and result.action == "init_volume" and view == "form" then
|
||||
formError = feedback
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
formPasswordKey += 1
|
||||
end
|
||||
dirty = true
|
||||
end)
|
||||
|
||||
panel.setWantsSecondTicks(true)
|
||||
|
||||
function onOpen(_context)
|
||||
view = "list"
|
||||
feedback = ""
|
||||
sendCommand("refresh")
|
||||
render()
|
||||
end
|
||||
|
||||
function update()
|
||||
if dirty then
|
||||
render()
|
||||
end
|
||||
end
|
||||
|
||||
function onCloseClicked()
|
||||
panel.close()
|
||||
end
|
||||
|
||||
function onRefresh()
|
||||
sendCommand("refresh")
|
||||
end
|
||||
|
||||
function onAdd()
|
||||
formMode = "add"
|
||||
resetForm()
|
||||
formAutoMount = false
|
||||
view = "form"
|
||||
render()
|
||||
end
|
||||
|
||||
function onInit()
|
||||
formMode = "init"
|
||||
resetForm()
|
||||
formAutoMount = true
|
||||
formSavePassfile = true
|
||||
view = "form"
|
||||
render()
|
||||
end
|
||||
|
||||
function onEdit()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
formMode = "edit"
|
||||
fillFormFrom(vol)
|
||||
view = "form"
|
||||
render()
|
||||
end
|
||||
|
||||
function onCancelForm()
|
||||
view = "list"
|
||||
formError = ""
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
render()
|
||||
end
|
||||
|
||||
function onFormName(value) formName = value end
|
||||
function onFormCipher(value) formCipher = value end
|
||||
function onFormMount(value) formMount = value end
|
||||
function onFormPassfile(value) formPassfile = value end
|
||||
function onFormPassword(value) formPassword = if type(value) == "string" then value else "" end
|
||||
function onFormPasswordConfirm(value) formPasswordConfirm = if type(value) == "string" then value else "" end
|
||||
|
||||
function onFormAllowOther(value)
|
||||
formAllowOther = value == "true"
|
||||
render()
|
||||
end
|
||||
function onFormReadOnly(value)
|
||||
formReadOnly = value == "true"
|
||||
render()
|
||||
end
|
||||
function onFormAutoMount(value)
|
||||
formAutoMount = value == "true"
|
||||
render()
|
||||
end
|
||||
function onFormPlaintextNames(value)
|
||||
formPlaintextNames = value == "true"
|
||||
render()
|
||||
end
|
||||
function onFormAesSiv(value)
|
||||
formAesSiv = value == "true"
|
||||
render()
|
||||
end
|
||||
function onFormSavePassfile(value)
|
||||
formSavePassfile = value == "true"
|
||||
if not formSavePassfile then
|
||||
formAutoMount = false
|
||||
end
|
||||
render()
|
||||
end
|
||||
|
||||
function onSaveForm()
|
||||
local name = noctalia.string.trim(formName)
|
||||
local cipher = noctalia.string.trim(formCipher)
|
||||
local mount = noctalia.string.trim(formMount)
|
||||
if name == "" or cipher == "" or mount == "" then
|
||||
formError = tr("panel.field.required")
|
||||
render()
|
||||
return
|
||||
end
|
||||
|
||||
if formMode == "init" then
|
||||
if formPassword == "" then
|
||||
formError = tr("panel.password_required")
|
||||
render()
|
||||
return
|
||||
end
|
||||
if formPassword ~= formPasswordConfirm then
|
||||
formError = tr("panel.field.password_mismatch")
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
formPasswordKey += 1
|
||||
render()
|
||||
return
|
||||
end
|
||||
formError = ""
|
||||
sendCommand("init_volume", {
|
||||
name = name,
|
||||
cipherDir = cipher,
|
||||
mountPoint = mount,
|
||||
password = formPassword,
|
||||
plaintextNames = formPlaintextNames,
|
||||
aesSiv = formAesSiv,
|
||||
savePassfile = formSavePassfile,
|
||||
autoMount = formAutoMount and formSavePassfile,
|
||||
allowOther = formAllowOther,
|
||||
readOnly = formReadOnly,
|
||||
})
|
||||
formPassword = ""
|
||||
formPasswordConfirm = ""
|
||||
formPasswordKey += 1
|
||||
render()
|
||||
return
|
||||
end
|
||||
|
||||
formError = ""
|
||||
local payload = {
|
||||
name = name,
|
||||
cipherDir = cipher,
|
||||
mountPoint = mount,
|
||||
passfile = noctalia.string.trim(formPassfile),
|
||||
allowOther = formAllowOther,
|
||||
readOnly = formReadOnly,
|
||||
autoMount = formAutoMount,
|
||||
}
|
||||
if formMode == "edit" then
|
||||
payload.id = formEditId
|
||||
sendCommand("update_volume", payload)
|
||||
else
|
||||
sendCommand("add_volume", payload)
|
||||
end
|
||||
render()
|
||||
end
|
||||
|
||||
function onRemove()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
sendCommand("remove_volume", { id = vol.id })
|
||||
end
|
||||
|
||||
-- Named onOpenMount so it does not override the panel lifecycle onOpen().
|
||||
function onOpenMount()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
sendCommand("open", { id = vol.id })
|
||||
end
|
||||
|
||||
local function openPasswordPrompt(vol, mode)
|
||||
passwordVolumeId = vol.id
|
||||
passwordVolumeName = vol.name
|
||||
passwordValue = ""
|
||||
passwordError = ""
|
||||
passwordMode = mode or "mount"
|
||||
passwordRememberKeyring = true
|
||||
passwordKey += 1
|
||||
view = "password"
|
||||
render()
|
||||
end
|
||||
|
||||
function onToggleMount()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
if vol.mounted then
|
||||
sendCommand("unmount", { id = vol.id })
|
||||
return
|
||||
end
|
||||
-- Prefer keyring or existing passfile without prompting.
|
||||
if vol.useKeyring == true or (type(vol.passfile) == "string" and vol.passfile ~= "") then
|
||||
sendCommand("mount", { id = vol.id })
|
||||
return
|
||||
end
|
||||
openPasswordPrompt(vol, "mount")
|
||||
end
|
||||
|
||||
function onRememberKeyring()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
-- Keep edit context: after save, return to list via cancel/result
|
||||
openPasswordPrompt(vol, "store_keyring")
|
||||
end
|
||||
|
||||
function onForgetKeyring()
|
||||
local vol = selectedVolume()
|
||||
if not vol then
|
||||
return
|
||||
end
|
||||
sendCommand("forget_keyring", { id = vol.id })
|
||||
-- Stay on edit form; snapshot watch will refresh useKeyring flag
|
||||
render()
|
||||
end
|
||||
|
||||
function onPasswordChange(value)
|
||||
passwordValue = if type(value) == "string" then value else ""
|
||||
end
|
||||
|
||||
function onPasswordRemember(value)
|
||||
passwordRememberKeyring = value == true or value == "true"
|
||||
end
|
||||
|
||||
function onCancelPassword()
|
||||
passwordValue = ""
|
||||
passwordError = ""
|
||||
passwordMode = "mount"
|
||||
view = "list"
|
||||
render()
|
||||
end
|
||||
|
||||
function onConfirmPassword()
|
||||
if snapshot.busy then
|
||||
return
|
||||
end
|
||||
if passwordValue == "" then
|
||||
passwordError = tr("panel.password_required")
|
||||
render()
|
||||
return
|
||||
end
|
||||
local pw = passwordValue
|
||||
local mode = passwordMode
|
||||
local id = passwordVolumeId
|
||||
passwordValue = ""
|
||||
passwordError = ""
|
||||
passwordMode = "mount"
|
||||
if mode == "store_keyring" then
|
||||
sendCommand("store_keyring", {
|
||||
id = id,
|
||||
password = pw,
|
||||
enableAutoMount = true,
|
||||
})
|
||||
else
|
||||
sendCommand("mount", {
|
||||
id = id,
|
||||
password = pw,
|
||||
storeKeyring = passwordRememberKeyring == true,
|
||||
})
|
||||
end
|
||||
passwordKey += 1
|
||||
view = "list"
|
||||
render()
|
||||
end
|
||||
Reference in New Issue
Block a user