Add Legacy panel mode, keyboard navigation and fixed recording issues. (#293)
This commit is contained in:
+28
-14
@@ -16,7 +16,7 @@ not the original implementation.
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | `alexander/screen-toolkit` |
|
| ID | `alexander/screen-toolkit` |
|
||||||
| Entries | Bar widget: `widget`; control-center shortcut: `toggle`; panels: `panel` (compact tools), `panel-full` (full tools), `result` (result view); service: `service` |
|
| Entries | Bar widget: `widget`; control-center shortcut: `toggle`; panels: `panel` (compact tools), `panel-full` (full tools), `panel-legacy` (legacy layout), `result` (result view); service: `service` |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ reported when that feature is started.
|
|||||||
- **`stat`** — recording file size
|
- **`stat`** — recording file size
|
||||||
- **`pkill`** — stopping active recording backends
|
- **`pkill`** — stopping active recording backends
|
||||||
- **`xdg-open`** — opening URLs, OCR search results, and shared-link targets
|
- **`xdg-open`** — opening URLs, OCR search results, and shared-link targets
|
||||||
|
- **`mpv`** — open recording preview in legacy mode subpanel
|
||||||
|
|
||||||
Recording requires at least one backend:
|
Recording requires at least one backend:
|
||||||
|
|
||||||
@@ -60,16 +61,24 @@ Settings → Control Center shortcuts. Left-click either one opens the main pane
|
|||||||
(or stops a recording); right-clicking the bar widget quick-picks a color. While
|
(or stops a recording); right-clicking the bar widget quick-picks a color. While
|
||||||
a recording is active, the widget and shortcut show a pulsing red dot.
|
a recording is active, the widget and shortcut show a pulsing red dot.
|
||||||
|
|
||||||
The main panel has two layouts, selected by the `panel-mode` setting (default:
|
The main panel has three layouts, selected by the `panel-mode` setting (default:
|
||||||
**Full**):
|
**Full**):
|
||||||
|
|
||||||
- **Full** — the spacious original grid with section titles (`panel-full`,
|
- **Full** — the spacious original grid with section titles (`panel-full`, 660×340).
|
||||||
660×340).
|
- **Compact** — a dense grid grouped into tinted sections (`panel`, 380×290).
|
||||||
- **Compact** — a dense grid grouped into tinted sections (`panel`,
|
- **Legacy** — recreates the Noctalia-v4 layout (`panel-legacy`, 380×260).
|
||||||
380×260).
|
|
||||||
|
### Legacy mode
|
||||||
|
Legacy mode recreates the original Noctalia v4 screen-toolkit layout:
|
||||||
|
|
||||||
|
> **Note:** The features below are exclusive to the **Legacy** layout.
|
||||||
|
|
||||||
|
- Dedicated Markup and Recording subpanels.
|
||||||
|
- Preview and one-click access to the most recent screenshot or recording.
|
||||||
|
- Quick microphone and system audio toggles in the `Record` subpanel.
|
||||||
|
|
||||||
The widget and shortcut open whichever entry matches the setting; the panel
|
The widget and shortcut open whichever entry matches the setting; the panel
|
||||||
footnote under Settings → Plugins shows both panels' placement/position
|
footnote under Settings → Plugins shows all three panels' placement/position
|
||||||
options.
|
options.
|
||||||
|
|
||||||
Toggle the tools panel (opens the entry matching the `panel-mode` setting):
|
Toggle the tools panel (opens the entry matching the `panel-mode` setting):
|
||||||
@@ -84,8 +93,7 @@ For example, bind it to `SUPER+P` in Hyprland:
|
|||||||
bind = SUPER, P, exec, noctalia msg plugin alexander/screen-toolkit:service all toggle
|
bind = SUPER, P, exec, noctalia msg plugin alexander/screen-toolkit:service all toggle
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `toggle` reads the `panel-mode` setting, this single bind works in both
|
Because `toggle` reads the `panel-mode` setting, this single bind works in all three layouts (Full, Compact and Legacy), so there is no need to change the keybind when you switch layouts.
|
||||||
full and compact mode — no need to change the keybind when you switch layouts.
|
|
||||||
|
|
||||||
Open the compact tools panel:
|
Open the compact tools panel:
|
||||||
|
|
||||||
@@ -99,6 +107,12 @@ Open the full tools panel (the original spacious grid):
|
|||||||
noctalia msg panel-toggle alexander/screen-toolkit:panel-full
|
noctalia msg panel-toggle alexander/screen-toolkit:panel-full
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Open the legacy tools panel (the 4×2 grid with subpanels):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
noctalia msg panel-toggle alexander/screen-toolkit:panel-legacy
|
||||||
|
```
|
||||||
|
|
||||||
Open the result panel (shows the last capture/recording output):
|
Open the result panel (shows the last capture/recording output):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -164,7 +178,7 @@ All settings live in Settings → Plugins (gear on the plugin's row).
|
|||||||
| `record-skip-confirmation` | `bool` | `false` | Save automatically when a recording ends, skipping the save dialog. |
|
| `record-skip-confirmation` | `bool` | `false` | Save automatically when a recording ends, skipping the save dialog. |
|
||||||
| `record-copy-to-clipboard` | `bool` | `false` | Finalize to MP4 and copy the file URI when recording ends. |
|
| `record-copy-to-clipboard` | `bool` | `false` | Finalize to MP4 and copy the file URI when recording ends. |
|
||||||
| `gif-max-seconds` | `int` | `30` | Cap for GIF recordings (1–600 s). |
|
| `gif-max-seconds` | `int` | `30` | Cap for GIF recordings (1–600 s). |
|
||||||
| `panel-mode` | `select` | `full` | Main panel layout: `full` (spacious original grid, 660×340) or `compact` (dense grid, 380×260). |
|
| `panel-mode` | `select` | `full` | Main panel layout: `full` (spacious original grid, 660×340) or `compact` (dense grid, 380×290) or `legacy` (4×2 legacy grid, 380×260). |
|
||||||
|
|
||||||
## IPC
|
## IPC
|
||||||
|
|
||||||
@@ -195,7 +209,7 @@ noctalia msg plugin alexander/screen-toolkit:service all clearHistory
|
|||||||
```
|
```
|
||||||
|
|
||||||
`toggle` opens the tools panel **matching the `panel-mode` setting** — one IPC
|
`toggle` opens the tools panel **matching the `panel-mode` setting** — one IPC
|
||||||
that works for both layouts. The bar widget and control-center shortcut use the
|
that works for all three layouts. The bar widget and control-center shortcut use the
|
||||||
same mode-aware toggle.
|
same mode-aware toggle.
|
||||||
|
|
||||||
Commands that take a payload:
|
Commands that take a payload:
|
||||||
@@ -244,10 +258,10 @@ Summary of every service command:
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **Two panel entries, one layout setting.** Panel size is host-owned: the host
|
- **Three panel entries, one layout setting.** Panel size is host-owned: the host
|
||||||
sizes each `[[panel]]` entry from its `width`/`height`, and there is no runtime
|
sizes each `[[panel]]` entry from its `width`/`height`, and there is no runtime
|
||||||
resize. So the two layouts are two entries sharing `panel.luau`: `panel-full`
|
resize. So the three layouts are three entries sharing `panel.luau`: `panel-full`
|
||||||
(660×340, the original spacious grid) and `panel` (380×260, the compact grid).
|
(660×340, the original spacious grid), `panel` (380×290, the compact grid) and `panel-legacy` (380×260, the legacy grid).
|
||||||
The `panel-mode` setting picks which one renders, and `toggle`/widget/shortcut
|
The `panel-mode` setting picks which one renders, and `toggle`/widget/shortcut
|
||||||
all open the matching entry. Changing the setting only affects which panel
|
all open the matching entry. Changing the setting only affects which panel
|
||||||
opens next time; an already-open panel keeps its current size until closed.
|
opens next time; an already-open panel keeps its current size until closed.
|
||||||
|
|||||||
+415
-142
@@ -11,9 +11,17 @@
|
|||||||
-- state channel. Capture tools close the panel first so the capture never
|
-- state channel. Capture tools close the panel first so the capture never
|
||||||
-- includes it.
|
-- includes it.
|
||||||
|
|
||||||
|
local activeView = "main"
|
||||||
|
local recordAudioOut = (noctalia.getConfig("record-audio-out") == true)
|
||||||
|
local recordAudioIn = false
|
||||||
|
local hoveredAudioIn = false
|
||||||
|
local hoveredAudioOut = false
|
||||||
|
local hoveredBack = false
|
||||||
|
|
||||||
local panelOpen = false
|
local panelOpen = false
|
||||||
local recording = noctalia.state.get("recording") == true
|
local recording = noctalia.state.get("recording") == true
|
||||||
local hoveredTool = nil
|
local hoveredTool = nil
|
||||||
|
local focusedTool local focusedIndex = 0
|
||||||
local hoveredClose = false
|
local hoveredClose = false
|
||||||
local hoveredStop = false
|
local hoveredStop = false
|
||||||
local render
|
local render
|
||||||
@@ -29,36 +37,48 @@ local function send(action, payload)
|
|||||||
noctalia.state.set("command", { action = action, payload = payload })
|
noctalia.state.set("command", { action = action, payload = payload })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function shellQuote(value)
|
||||||
|
return "'" .. tostring(value):gsub("'", [["'"']]) .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
local function getMode()
|
local function getMode()
|
||||||
return noctalia.getConfig("panel-mode") == "full" and "full" or "compact"
|
local mode = noctalia.getConfig("panel-mode")
|
||||||
|
if mode == "full" or mode == "compact" or mode == "legacy" then return mode end
|
||||||
|
return "full"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Capture tools must not see the panel in the frame, so close it first.
|
-- Capture tools must not see the panel in the frame, so close it first.
|
||||||
local CAPTURE_ACTIONS = {
|
local CAPTURE_ACTIONS = {
|
||||||
colorPicker = true,
|
colorPicker = true, ocr = true, qr = true, palette = true, lens = true, measure = true,
|
||||||
ocr = true,
|
annotate = true, annotateFullscreen = true, annotateWindow = true,
|
||||||
qr = true,
|
record = true, recordMp4 = true, recordFullscreen = true, recordFullscreenMp4 = true,
|
||||||
palette = true,
|
|
||||||
lens = true,
|
|
||||||
measure = true,
|
|
||||||
annotate = true,
|
|
||||||
annotateFullscreen = true,
|
|
||||||
annotateWindow = true,
|
|
||||||
record = true,
|
|
||||||
recordMp4 = true,
|
|
||||||
recordFullscreen = true,
|
|
||||||
recordFullscreenMp4 = true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function runTool(action)
|
local function runTool(action)
|
||||||
if CAPTURE_ACTIONS[action] then
|
if action == "record" or action == "recordFullscreen" then noctalia.state.set("recordFormat", "gif")
|
||||||
panel.close()
|
elseif action == "recordMp4" or action == "recordFullscreenMp4" then noctalia.state.set("recordFormat", "mp4") end
|
||||||
end
|
if CAPTURE_ACTIONS[action] then panel.close() end
|
||||||
send(action)
|
send(action)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Tool data ──────────────────────────────────────────────────────────────
|
local RECORD_THUMB = "/tmp/screen-toolkit-record-thumb.png"
|
||||||
|
local ANNOTATE_THUMB = "/tmp/screen-toolkit-annotate.png"
|
||||||
|
local lastCapturePath, lastRecordPath = nil, nil
|
||||||
|
|
||||||
|
local function refreshPreview()
|
||||||
|
local annotateResult = noctalia.state.get("annotateResult")
|
||||||
|
local recordPath = noctalia.state.get("recordPath")
|
||||||
|
if annotateResult and type(annotateResult) == "table" and annotateResult.capturePath and annotateResult.capturePath ~= "" then
|
||||||
|
lastCapturePath = annotateResult.capturePath
|
||||||
|
elseif noctalia.fileExists and noctalia.fileExists(ANNOTATE_THUMB) then
|
||||||
|
lastCapturePath = ANNOTATE_THUMB
|
||||||
|
end
|
||||||
|
if recordPath and type(recordPath) == "string" and recordPath ~= "" then lastRecordPath = recordPath end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Tool data ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- For Compact-mode and Full-mode layouts
|
||||||
local TOOLS = {
|
local TOOLS = {
|
||||||
{ action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" },
|
{ action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" },
|
||||||
{ action = "ocr", label_key = "tools.ocr", glyph = "scan" },
|
{ action = "ocr", label_key = "tools.ocr", glyph = "scan" },
|
||||||
@@ -76,148 +96,185 @@ local ANNOTATE_TOOLS = {
|
|||||||
|
|
||||||
local RECORD_TOOLS = {
|
local RECORD_TOOLS = {
|
||||||
{ action = "record", label_key = "tools.record_gif", glyph = "gif" },
|
{ action = "record", label_key = "tools.record_gif", glyph = "gif" },
|
||||||
{ action = "recordMp4", label_key = "tools.record_mp4", glyph = "movie" },
|
{ action = "recordMp4", label_key = "tools.record_mp4", glyph = "video" },
|
||||||
{ action = "recordFullscreen", label_key = "tools.record_fullscreen_gif", glyph = "video" },
|
{ action = "recordFullscreen", label_key = "tools.record_fullscreen_gif", glyph = "gif" },
|
||||||
{ action = "recordFullscreenMp4", label_key = "tools.record_fullscreen_mp4", glyph = "video" },
|
{ action = "recordFullscreenMp4", label_key = "tools.record_fullscreen_mp4", glyph = "video" },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- For Legacy-mode layout
|
||||||
|
local TOOLS_LEGACY = {
|
||||||
|
{ id = "colorPicker", action = "colorPicker", label_key = "tools.colorpicker", glyph = "palette" },
|
||||||
|
{ id = "palette", action = "palette", label_key = "tools.palette", glyph = "color-swatch" },
|
||||||
|
{ id = "ocr", action = "ocr", label_key = "tools.ocr", glyph = "scan" },
|
||||||
|
{ id = "qr", action = "qr", label_key = "tools.qr", glyph = "qrcode" },
|
||||||
|
{ id = "annotate", action = "view_annotate", label_key = "tools.annotate", glyph = "pencil" },
|
||||||
|
{ id = "record", action = "view_record", label_key = "tools.record", glyph = "video" },
|
||||||
|
{ id = "measure", action = "measure", label_key = "tools.measure", glyph = "ruler" },
|
||||||
|
{ id = "lens", action = "lens", label_key = "tools.lens", glyph = "search" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ── Keyboard Navigation State & Helpers ────────────────────────────────────
|
||||||
|
|
||||||
|
local function getActiveItems()
|
||||||
|
local mode = getMode()
|
||||||
|
if mode == "compact" or mode == "full" then
|
||||||
|
local list = {}
|
||||||
|
for _, t in ipairs(TOOLS) do table.insert(list, { type = "tool", action = t.action }) end
|
||||||
|
for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end
|
||||||
|
for _, t in ipairs(RECORD_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end
|
||||||
|
return list
|
||||||
|
else
|
||||||
|
if activeView == "annotate" then
|
||||||
|
local list = { { type = "back" } }
|
||||||
|
for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end
|
||||||
|
table.insert(list, { type = "preview_screenshot" })
|
||||||
|
return list
|
||||||
|
elseif activeView == "record" then
|
||||||
|
local list = { { type = "back" } }
|
||||||
|
for _, t in ipairs(RECORD_TOOLS) do table.insert(list, { type = "tool", action = t.action }) end
|
||||||
|
table.insert(list, { type = "sysAudio" })
|
||||||
|
table.insert(list, { type = "micAudio" })
|
||||||
|
if recording then table.insert(list, { type = "stopBtn" }) end
|
||||||
|
table.insert(list, { type = "preview_recording" })
|
||||||
|
return list
|
||||||
|
else
|
||||||
|
local list = {}
|
||||||
|
for _, t in ipairs(TOOLS_LEGACY) do table.insert(list, { type = "legacy_tile", item = t }) end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateFocusedTool()
|
||||||
|
local items = getActiveItems()
|
||||||
|
if #items == 0 or focusedIndex == 0 or focusedIndex == nil then
|
||||||
|
focusedTool = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if focusedIndex > #items then focusedIndex = #items end
|
||||||
|
if focusedIndex < 1 then focusedIndex = 1 end
|
||||||
|
local cur = items[focusedIndex]
|
||||||
|
if cur then
|
||||||
|
if cur.type == "tool" then focusedTool = cur.action
|
||||||
|
elseif cur.type == "legacy_tile" then focusedTool = cur.item.id
|
||||||
|
else focusedTool = cur.type end
|
||||||
|
else
|
||||||
|
focusedTool = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Preview handlers for record and annotate subpanels.
|
||||||
|
local function openScreenshotPreview()
|
||||||
|
refreshPreview()
|
||||||
|
local targetPath = lastCapturePath or (noctalia.fileExists and noctalia.fileExists(ANNOTATE_THUMB) and ANNOTATE_THUMB or nil)
|
||||||
|
if not targetPath or targetPath == "" or (noctalia.fileExists and not noctalia.fileExists(targetPath)) then
|
||||||
|
noctalia.notifyError("No screenshot preview available yet. Take a capture first!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if noctalia.commandExists("satty") then noctalia.runAsync("satty --filename " .. shellQuote(targetPath))
|
||||||
|
elseif noctalia.commandExists("swappy") then noctalia.runAsync("swappy -f " .. shellQuote(targetPath))
|
||||||
|
elseif noctalia.commandExists("gimp") then noctalia.runAsync("gimp " .. shellQuote(targetPath))
|
||||||
|
else noctalia.notifyError(tr("messages.missing_dep", { dep = "satty (or swappy/gimp)" })) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function openRecordingPreview()
|
||||||
|
refreshPreview()
|
||||||
|
local targetPath = lastRecordPath
|
||||||
|
if not targetPath or targetPath == "" or (noctalia.fileExists and not noctalia.fileExists(targetPath)) then
|
||||||
|
noctalia.notifyError("No recording preview available yet. Make a recording first!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if noctalia.commandExists("mpv") then noctalia.runAsync("mpv --no-config --title='Recording Preview' " .. shellQuote(targetPath))
|
||||||
|
else noctalia.notifyError(tr("messages.missing_dep", { dep = "mpv" })) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function headerActions(includeClose)
|
||||||
|
local actions = {}
|
||||||
|
if recording then
|
||||||
|
table.insert(actions, ui.button({
|
||||||
|
key = "header-stop-btn", glyph = "circle", variant = "destructive", controlSize = "sm", tooltip = tr("panel.stop"),
|
||||||
|
onClick = function() hoveredStop = false; send("recordStop") end,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
if includeClose then
|
||||||
|
table.insert(actions, ui.button({
|
||||||
|
key = "header-close-btn", glyph = "close", onClick = function() panel.close() end,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
return ui.row({ gap = 4 }, actions)
|
||||||
|
end
|
||||||
|
|
||||||
-- ── Compact mode ───────────────────────────────────────────────────────────
|
-- ── Compact mode ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
local function compactHeader()
|
local function compactHeader()
|
||||||
local actions = {
|
|
||||||
ui.column({
|
|
||||||
key = "close-button",
|
|
||||||
width = 24,
|
|
||||||
height = 24,
|
|
||||||
radius = 9,
|
|
||||||
fill = hoveredClose and "primary/0.12" or nil,
|
|
||||||
border = hoveredClose and "primary" or "outline",
|
|
||||||
borderWidth = 1,
|
|
||||||
align = "center",
|
|
||||||
justify = "center",
|
|
||||||
onClick = function() panel.close() end,
|
|
||||||
onHover = function(state)
|
|
||||||
hoveredClose = state == "true"
|
|
||||||
render()
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
ui.glyph({ name = "close", size = 14, color = hoveredClose and "on_surface" or "on_surface_variant" }),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
if recording then
|
|
||||||
table.insert(actions, 1, ui.column({
|
|
||||||
key = "stop-button",
|
|
||||||
width = 24,
|
|
||||||
height = 24,
|
|
||||||
radius = 9,
|
|
||||||
fill = "error",
|
|
||||||
border = hoveredStop and "on_surface" or nil,
|
|
||||||
borderWidth = hoveredStop and 2 or 0,
|
|
||||||
align = "center",
|
|
||||||
justify = "center",
|
|
||||||
tooltip = tr("panel.stop"),
|
|
||||||
onClick = function() send("recordStop") end,
|
|
||||||
onHover = function(state)
|
|
||||||
hoveredStop = state == "true"
|
|
||||||
render()
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
ui.glyph({ name = "square", size = 12, color = "on_surface" }),
|
|
||||||
}))
|
|
||||||
end
|
|
||||||
return ui.row({ align = "center", gap = 6 }, {
|
return ui.row({ align = "center", gap = 6 }, {
|
||||||
ui.glyph({ name = "crosshair", size = 16, color = "primary" }),
|
ui.glyph({ name = "crosshair", size = 16, color = "primary" }),
|
||||||
ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 16, fontWeight = "bold", color = "on_surface" }),
|
ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 16, fontWeight = "bold", color = "on_surface" }),
|
||||||
ui.row({ gap = 4 }, actions),
|
headerActions(true),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function setHoveredTool(action)
|
||||||
|
hoveredTool = action
|
||||||
|
if action then
|
||||||
|
for idx, item in ipairs(getActiveItems()) do
|
||||||
|
local id = (item.type == "tool") and item.action or ((item.type == "legacy_tile") and item.item.id or item.type)
|
||||||
|
if id == action then focusedIndex = idx; break end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
focusedIndex = 0
|
||||||
|
end
|
||||||
|
updateFocusedTool()
|
||||||
|
end
|
||||||
|
|
||||||
local function compactTile(t)
|
local function compactTile(t)
|
||||||
local color = hoveredTool == t.action and "primary" or "on_surface"
|
local isFocused = (hoveredTool == t.action) or (focusedTool == t.action)
|
||||||
|
local color = isFocused and "primary" or "on_surface"
|
||||||
return ui.column({
|
return ui.column({
|
||||||
key = "tile-" .. t.action,
|
key = "tile-" .. t.action, flexGrow = 1, gap = 2, align = "center", paddingV = 4, radius = 8,
|
||||||
flexGrow = 1,
|
fill = isFocused and "primary/0.15" or "surface_variant", border = isFocused and "primary" or "surface_variant", borderWidth = isFocused and 1 or 0,
|
||||||
gap = 2,
|
onClick = function() runTool(t.action) end,
|
||||||
align = "center",
|
onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end,
|
||||||
}, {
|
}, {
|
||||||
ui.column({
|
ui.column({ width = 30, height = 30, radius = 9, fill = isFocused and "primary/0.2" or "on_primary", border = isFocused and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, {
|
||||||
gap = 2,
|
ui.glyph({ name = t.glyph, size = 16, color = color }),
|
||||||
align = "center",
|
|
||||||
onClick = function() runTool(t.action) end,
|
|
||||||
onHover = function(state)
|
|
||||||
hoveredTool = (state == "true") and t.action or nil
|
|
||||||
render()
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
ui.column({ width = 30, height = 30, radius = 9, fill = "on_primary", border = color == "primary" and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, {
|
|
||||||
ui.glyph({ name = t.glyph, size = 16, color = color }),
|
|
||||||
}),
|
|
||||||
ui.label({ text = tr(t.label_key), maxWidth = 100, maxLines = 1, textAlign = "center", fontSize = 11, color = color }),
|
|
||||||
}),
|
}),
|
||||||
|
ui.label({ text = tr(t.label_key), maxWidth = 100, maxLines = 1, textAlign = "center", fontSize = 11, color = color }),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function compactToolsRow(tools)
|
local function compactToolsRow(tools)
|
||||||
local row = {}
|
local row = {}
|
||||||
for _, t in ipairs(tools) do
|
for _, t in ipairs(tools) do row[#row + 1] = compactTile(t) end
|
||||||
row[#row + 1] = compactTile(t)
|
|
||||||
end
|
|
||||||
return ui.row({ flexGrow = 1, gap = 2 }, row)
|
return ui.row({ flexGrow = 1, gap = 2 }, row)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function compactSection(tools)
|
local function compactSection(tools)
|
||||||
return ui.column({ gap = 4, paddingV = 8, paddingH = 6, radius = 12, fill = "surface_variant" }, {
|
return ui.column({ gap = 4, paddingV = 8, paddingH = 6, radius = 12, fill = "surface_variant" }, { compactToolsRow(tools) })
|
||||||
compactToolsRow(tools),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function compactGrid()
|
local function compactGrid()
|
||||||
return ui.column({ gap = 6 }, {
|
return ui.column({ gap = 6 }, { compactSection(TOOLS), compactSection(ANNOTATE_TOOLS), compactSection(RECORD_TOOLS) })
|
||||||
compactSection(TOOLS),
|
|
||||||
compactSection(ANNOTATE_TOOLS),
|
|
||||||
compactSection(RECORD_TOOLS),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Full mode ──────────────────────────────────────────────────────────────
|
-- ── Full mode ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
local function fullHeader()
|
local function fullHeader()
|
||||||
local actions = {
|
|
||||||
ui.button({ glyph = "close", onClick = function() panel.close() end }),
|
|
||||||
}
|
|
||||||
if recording then
|
|
||||||
table.insert(actions, 1, ui.button({
|
|
||||||
glyph = "square",
|
|
||||||
variant = "destructive",
|
|
||||||
controlSize = "sm",
|
|
||||||
tooltip = tr("panel.stop"),
|
|
||||||
onClick = function() send("recordStop") end,
|
|
||||||
}))
|
|
||||||
end
|
|
||||||
return ui.row({ align = "center", gap = 10 }, {
|
return ui.row({ align = "center", gap = 10 }, {
|
||||||
ui.glyph({ name = "crosshair", size = 24, color = "primary" }),
|
ui.glyph({ name = "crosshair", size = 24, color = "primary" }),
|
||||||
ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 22, fontWeight = "bold", color = "on_surface" }),
|
ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 22, fontWeight = "bold", color = "on_surface" }),
|
||||||
ui.row({ gap = 4 }, actions),
|
headerActions(true),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function fullTile(t)
|
local function fullTile(t)
|
||||||
local color = hoveredTool == t.action and "primary" or "on_surface"
|
local isFocused = (hoveredTool == t.action) or (focusedTool == t.action)
|
||||||
|
local color = isFocused and "primary" or "on_surface"
|
||||||
return ui.column({
|
return ui.column({
|
||||||
key = "tile-" .. t.action,
|
key = "tile-" .. t.action, flexGrow = 1, gap = 6, align = "center", paddingV = 6, radius = 12,
|
||||||
flexGrow = 1,
|
fill = "surface_variant", borderWidth = 1, border = color == "primary" and "primary" or "outline",
|
||||||
gap = 6,
|
|
||||||
align = "center",
|
|
||||||
paddingV = 6,
|
|
||||||
radius = 12,
|
|
||||||
fill = "surface_variant",
|
|
||||||
borderWidth = 1,
|
|
||||||
border = color == "primary" and "primary" or "outline",
|
|
||||||
onClick = function() runTool(t.action) end,
|
onClick = function() runTool(t.action) end,
|
||||||
onHover = function(state)
|
onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end,
|
||||||
hoveredTool = (state == "true") and t.action or nil
|
|
||||||
render()
|
|
||||||
end,
|
|
||||||
}, {
|
}, {
|
||||||
ui.column({ width = 30, height = 30, radius = 9, fill = "on_primary", border = color == "primary" and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, {
|
ui.column({ width = 30, height = 30, radius = 9, fill = "on_primary", border = color == "primary" and "primary" or "on_primary", borderWidth = 1, align = "center", justify = "center" }, {
|
||||||
ui.glyph({ name = t.glyph, size = 16, color = color }),
|
ui.glyph({ name = t.glyph, size = 16, color = color }),
|
||||||
@@ -228,9 +285,7 @@ end
|
|||||||
|
|
||||||
local function fullToolsRow(tools)
|
local function fullToolsRow(tools)
|
||||||
local row = {}
|
local row = {}
|
||||||
for _, t in ipairs(tools) do
|
for _, t in ipairs(tools) do row[#row + 1] = fullTile(t) end
|
||||||
row[#row + 1] = fullTile(t)
|
|
||||||
end
|
|
||||||
return ui.row({ flexGrow = 1, gap = 6 }, row)
|
return ui.row({ flexGrow = 1, gap = 6 }, row)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -240,43 +295,261 @@ end
|
|||||||
|
|
||||||
local function fullGrid()
|
local function fullGrid()
|
||||||
return ui.column({ flexGrow = 1, gap = 4, justify = "space_between" }, {
|
return ui.column({ flexGrow = 1, gap = 4, justify = "space_between" }, {
|
||||||
sectionLabel(tr("panel.section_capture")),
|
sectionLabel(tr("panel.section_capture")), fullToolsRow(TOOLS),
|
||||||
fullToolsRow(TOOLS),
|
sectionLabel(tr("panel.section_annotate")), fullToolsRow(ANNOTATE_TOOLS),
|
||||||
sectionLabel(tr("panel.section_annotate")),
|
sectionLabel(tr("panel.section_record")), fullToolsRow(RECORD_TOOLS),
|
||||||
fullToolsRow(ANNOTATE_TOOLS),
|
|
||||||
sectionLabel(tr("panel.section_record")),
|
|
||||||
fullToolsRow(RECORD_TOOLS),
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ── Legacy mode ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function legacyHeader()
|
||||||
|
return ui.row({ align = "center", gap = 5 }, {
|
||||||
|
ui.glyph({ name = "crosshair", size = 18, color = "primary" }),
|
||||||
|
ui.label({ text = tr("panel.title"), flexGrow = 1, fontSize = 18, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
headerActions(false),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SubpanelHeader(title)
|
||||||
|
local isBackFocused = hoveredBack or (focusedTool == "back")
|
||||||
|
return ui.row({ align = "center", gap = 5 }, {
|
||||||
|
ui.glyph({ name = "crosshair", size = 18, color = "primary" }),
|
||||||
|
ui.label({ text = title, flexGrow = 1, fontSize = 18, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.column({
|
||||||
|
key = "back-button", height = 30, width = 30, paddingH = 0, radius = 9,
|
||||||
|
fill = isBackFocused and "primary/0.12" or "surface", border = isBackFocused and "primary" or "surface", borderWidth = 1,
|
||||||
|
align = "center", justify = "center",
|
||||||
|
onClick = function() activeView = "main"; focusedIndex = 0; updateFocusedTool(); render() end,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and "back" or nil); render() end,
|
||||||
|
}, {
|
||||||
|
ui.row({ align = "center", justify = "center", gap = 4 }, {
|
||||||
|
ui.glyph({ name = "chevron-left", size = 16, color = isBackFocused and "primary" or "on_surface" }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function legacyTile(t)
|
||||||
|
local isFocused = (hoveredTool == t.id) or (focusedTool == t.id)
|
||||||
|
local color = isFocused and "primary" or "on_surface"
|
||||||
|
local onClick = function()
|
||||||
|
if t.action == "view_annotate" then activeView = "annotate"; focusedIndex = 0; updateFocusedTool(); render()
|
||||||
|
elseif t.action == "view_record" then activeView = "record"; focusedIndex = 0; updateFocusedTool(); render()
|
||||||
|
else runTool(t.action) end
|
||||||
|
end
|
||||||
|
return ui.column({
|
||||||
|
key = "tile-" .. t.id, flexGrow = 1, gap = 2, paddingV = 5, paddingH = 0, align = "center", justify = "center", onClick = onClick,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and t.id or nil); render() end,
|
||||||
|
}, {
|
||||||
|
ui.column({ width = 50, height = 50, radius = 15, fill = "on_primary", border = color == "primary" and "primary" or "outline", borderWidth = isFocused and 2 or 0, align = "center", justify = "center" }, {
|
||||||
|
ui.glyph({ name = t.glyph, size = 25, color = color }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = tr(t.label_key), maxWidth = 80, maxLines = 1, textAlign = "center", fontSize = 11, color = color }),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function legacyGrid()
|
||||||
|
local row1, row2 = {}, {}
|
||||||
|
for i, t in ipairs(TOOLS_LEGACY) do table.insert(i <= 4 and row1 or row2, legacyTile(t)) end
|
||||||
|
return ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, {
|
||||||
|
ui.row({ flexGrow = 1, gap = 0 }, row1), ui.row({ flexGrow = 1, gap = 0 }, row2),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Subpanels for legacy mode: annotate and record.
|
||||||
|
|
||||||
|
local function subpanelButton(t, paddingV)
|
||||||
|
local isFocused = (hoveredTool == t.action) or (focusedTool == t.action)
|
||||||
|
local color = isFocused and "primary" or "on_surface"
|
||||||
|
return ui.column({
|
||||||
|
key = "btn-" .. t.action, paddingV = paddingV, paddingH = 5, radius = 8,
|
||||||
|
fill = isFocused and "primary/0.12" or "surface", border = isFocused and "primary" or "surface", borderWidth = 1,
|
||||||
|
onClick = function() runTool(t.action) end,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and t.action or nil); render() end,
|
||||||
|
}, {
|
||||||
|
ui.row({ align = "center", gap = 6 }, {
|
||||||
|
ui.glyph({ name = t.glyph, size = 15, color = color }),
|
||||||
|
ui.label({ text = tr(t.label_key), fontSize = 12, fontWeight = "bold", color = color }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function MarkupSubpanel()
|
||||||
|
refreshPreview()
|
||||||
|
local leftButtons = {}
|
||||||
|
for _, t in ipairs(ANNOTATE_TOOLS) do table.insert(leftButtons, subpanelButton(t, 6)) end
|
||||||
|
local fileExists = noctalia.fileExists
|
||||||
|
local hasCaptureFile = lastCapturePath and lastCapturePath ~= "" and (not fileExists or fileExists(lastCapturePath))
|
||||||
|
local isPreviewFocused = (focusedTool == "preview_screenshot")
|
||||||
|
local rightPreview = ui.column({
|
||||||
|
width = 200, height = 150, align = "center", justify = "center", padding = hasCaptureFile and 4 or 8, radius = 10, fill = "surface",
|
||||||
|
border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openScreenshotPreview,
|
||||||
|
}, hasCaptureFile and {
|
||||||
|
ui.image({ path = lastCapturePath, width = 200, height = 130, fit = "contain", radius = 6 }),
|
||||||
|
ui.label({ text = "Click to open in Satty", fontSize = 10, fontWeight = "bold", color = "primary" }),
|
||||||
|
} or {
|
||||||
|
ui.glyph({ name = "pencil", size = 28, color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = "No screenshot yet", fontSize = 11, color = "on_surface_variant" }),
|
||||||
|
})
|
||||||
|
return ui.column({ flexGrow = 1, gap = 10 }, {
|
||||||
|
SubpanelHeader("Markup"),
|
||||||
|
ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, {
|
||||||
|
ui.row({ flexGrow = 1, gap = 10 }, { ui.column({ width = 100, gap = 6 }, leftButtons), rightPreview }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RecordSubpanel()
|
||||||
|
refreshPreview()
|
||||||
|
local leftButtons = {}
|
||||||
|
for _, t in ipairs(RECORD_TOOLS) do table.insert(leftButtons, subpanelButton(t, 4)) end
|
||||||
|
local stopFocused = hoveredStop or (focusedTool == "stopBtn")
|
||||||
|
local stopBtn = ui.column({
|
||||||
|
key = "btn-stop-record", width = 28, height = 28, radius = 6, fill = "error", border = stopFocused and "primary" or "error", borderWidth = stopFocused and 2 or 1, align = "center", justify = "center",
|
||||||
|
onClick = function() send("recordStop") end,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and "stopBtn" or nil); render() end,
|
||||||
|
}, { ui.glyph({ name = "circle", size = 16, color = "on_primary", align = "center", justify = "center" }) })
|
||||||
|
|
||||||
|
local sysAudioFocused = hoveredAudioOut or (focusedTool == "sysAudio")
|
||||||
|
local sysAudioBtn = ui.column({
|
||||||
|
key = "toggle-sys-audio", width = 28, height = 28, radius = 6,
|
||||||
|
fill = recordAudioOut and (sysAudioFocused and "primary/0.25" or "primary/0.15") or (sysAudioFocused and "surface_variant" or "surface_variant/0.5"),
|
||||||
|
border = (recordAudioOut or sysAudioFocused) and "primary" or "outline", borderWidth = 1, align = "center", justify = "center",
|
||||||
|
onClick = function() recordAudioOut = not recordAudioOut; send("setAudioOut", recordAudioOut); render() end,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and "sysAudio" or nil); render() end,
|
||||||
|
}, { ui.glyph({ name = recordAudioOut and "volume-2" or "volume-x", size = 16, color = (recordAudioOut or sysAudioFocused) and "primary" or "on_surface_variant" }) })
|
||||||
|
|
||||||
|
local micAudioFocused = hoveredAudioIn or (focusedTool == "micAudio")
|
||||||
|
local micAudioBtn = ui.column({
|
||||||
|
key = "toggle-mic-audio", width = 28, height = 28, radius = 6,
|
||||||
|
fill = recordAudioIn and (micAudioFocused and "primary/0.25" or "primary/0.15") or (micAudioFocused and "surface_variant" or "surface_variant/0.5"),
|
||||||
|
border = (recordAudioIn or micAudioFocused) and "primary" or "outline", borderWidth = 1, align = "center", justify = "center",
|
||||||
|
onClick = function() recordAudioIn = not recordAudioIn; send("setAudioIn", recordAudioIn); render() end,
|
||||||
|
onHover = function(state) setHoveredTool((state == "true") and "micAudio" or nil); render() end,
|
||||||
|
}, { ui.glyph({ name = recordAudioIn and "microphone" or "microphone-off", size = 16, color = (recordAudioIn or micAudioFocused) and "primary" or "on_surface_variant" }) })
|
||||||
|
|
||||||
|
local controlsList = { sysAudioBtn, micAudioBtn }
|
||||||
|
if recording then table.insert(controlsList, 1, stopBtn) end
|
||||||
|
local controlRow = ui.row({ gap = 6, align = "center", justify = "center" }, controlsList)
|
||||||
|
local fileExists = noctalia.fileExists
|
||||||
|
local hasRecordFile = lastRecordPath and lastRecordPath ~= "" and (not fileExists or fileExists(lastRecordPath))
|
||||||
|
local hasRecordThumb = hasRecordFile and fileExists and fileExists(RECORD_THUMB)
|
||||||
|
local isPreviewFocused = (focusedTool == "preview_recording")
|
||||||
|
local rightPreview
|
||||||
|
if hasRecordFile or hasRecordThumb then
|
||||||
|
local thumbElement = hasRecordThumb and ui.image({ path = RECORD_THUMB, width = 240, height = 115, fit = "contain", radius = 6 }) or ui.glyph({ name = "movie", size = 28, color = "primary" })
|
||||||
|
local filename = lastRecordPath and (lastRecordPath:match("([^/]+)$") or lastRecordPath) or "Saved Recording"
|
||||||
|
rightPreview = ui.column({
|
||||||
|
width = 200, height = 150, align = "center", justify = "center", padding = hasRecordFile and 4 or 8, radius = 10, fill = "surface",
|
||||||
|
border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openRecordingPreview,
|
||||||
|
}, { thumbElement, ui.label({ text = filename, maxWidth = 160, maxLines = 1, fontSize = 9, color = "on_surface_variant" }), ui.label({ text = "Click to play in MPV", fontSize = 10, fontWeight = "bold", color = "primary" }) })
|
||||||
|
else
|
||||||
|
rightPreview = ui.column({
|
||||||
|
width = 200, height = 130, align = "center", justify = "center", padding = 8, radius = 10, fill = "surface",
|
||||||
|
border = isPreviewFocused and "primary" or "outline", borderWidth = isPreviewFocused and 2 or 1, onClick = openRecordingPreview,
|
||||||
|
}, { ui.glyph({ name = "video", size = 28, color = "on_surface_variant" }), ui.label({ text = "No recording yet", fontSize = 11, color = "on_surface_variant" }) })
|
||||||
|
end
|
||||||
|
local leftColumn = ui.column({ width = 100, gap = 6, align = "center" }, {
|
||||||
|
ui.column({ width = 100, gap = 6 }, leftButtons),
|
||||||
|
controlRow,
|
||||||
|
})
|
||||||
|
return ui.column({ flexGrow = 1, gap = 10 }, {
|
||||||
|
SubpanelHeader("Record"),
|
||||||
|
ui.column({ gap = 10, paddingV = 15, paddingH = 20, radius = 18, fill = "surface_variant" }, {
|
||||||
|
ui.row({ flexGrow = 1, gap = 10 }, { leftColumn, rightPreview }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Keyboard Handler ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function onKey(chord, pressed)
|
||||||
|
if not pressed then return end
|
||||||
|
local key = tostring(chord):lower()
|
||||||
|
local items = getActiveItems()
|
||||||
|
if #items == 0 then return end
|
||||||
|
local mode = getMode()
|
||||||
|
if chord == "Escape" then panel.close() return end
|
||||||
|
|
||||||
|
-- If nothing is focused yet (focusedIndex == 0), pressing arrow keys/tab takes focus directly to 1st square
|
||||||
|
if focusedIndex == 0 or focusedIndex == nil then
|
||||||
|
if key == "tab" or key == "right" or key == "left" or key == "backtab" or key == "shift+tab" or key == "down" or key == "up" then
|
||||||
|
focusedIndex = 1
|
||||||
|
updateFocusedTool()
|
||||||
|
render()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if key == "tab" or key == "right" then
|
||||||
|
focusedIndex = (focusedIndex % #items) + 1
|
||||||
|
elseif key == "backtab" or key == "left" then
|
||||||
|
focusedIndex = (focusedIndex - 2) % #items + 1
|
||||||
|
elseif key == "down" then
|
||||||
|
if mode == "legacy" and activeView == "main" then
|
||||||
|
focusedIndex = focusedIndex <= 4 and (focusedIndex + 4) or (focusedIndex - 4)
|
||||||
|
elseif mode == "compact" or mode == "full" then
|
||||||
|
focusedIndex = (focusedIndex <= 6 and math.min(7 + math.floor((focusedIndex - 1) / 2), 9)) or (focusedIndex <= 9 and (10 + (focusedIndex - 7)) or ((focusedIndex - 10) + 1))
|
||||||
|
else
|
||||||
|
focusedIndex = (focusedIndex % #items) + 1
|
||||||
|
end
|
||||||
|
elseif key == "up" then
|
||||||
|
if mode == "legacy" and activeView == "main" then
|
||||||
|
focusedIndex = focusedIndex > 4 and (focusedIndex - 4) or (focusedIndex + 4)
|
||||||
|
elseif mode == "compact" or mode == "full" then
|
||||||
|
focusedIndex = (focusedIndex >= 10 and math.min(7 + (focusedIndex - 10), 9)) or (focusedIndex >= 7 and ((focusedIndex - 7) * 2 + 1) or math.min(10 + math.floor((focusedIndex - 1) / 2), 13))
|
||||||
|
else
|
||||||
|
focusedIndex = (focusedIndex - 2) % #items + 1
|
||||||
|
end
|
||||||
|
elseif key == "return" or key == "enter" or key == "space" then
|
||||||
|
local current = items[focusedIndex]
|
||||||
|
if current then
|
||||||
|
if current.type == "tool" then runTool(current.action)
|
||||||
|
elseif current.type == "legacy_tile" then
|
||||||
|
if current.item.action == "view_annotate" then activeView = "annotate"; focusedIndex = 0
|
||||||
|
elseif current.item.action == "view_record" then activeView = "record"; focusedIndex = 0
|
||||||
|
else runTool(current.item.action) end
|
||||||
|
elseif current.type == "back" then activeView = "main"; focusedIndex = 0
|
||||||
|
elseif current.type == "sysAudio" then recordAudioOut = not recordAudioOut; send("setAudioOut", recordAudioOut)
|
||||||
|
elseif current.type == "micAudio" then recordAudioIn = not recordAudioIn; send("setAudioIn", recordAudioIn)
|
||||||
|
elseif current.type == "stopBtn" then send("recordStop")
|
||||||
|
elseif current.type == "preview_screenshot" then openScreenshotPreview()
|
||||||
|
elseif current.type == "preview_recording" then openRecordingPreview() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
updateFocusedTool()
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
-- ── Main render ────────────────────────────────────────────────────────────
|
-- ── Main render ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
render = function()
|
render = function()
|
||||||
local body
|
updateFocusedTool()
|
||||||
if getMode() == "full" then
|
local mode = getMode()
|
||||||
body = ui.column({ flexGrow = 1, gap = 10 }, {
|
local body = (mode == "full" and ui.column({ flexGrow = 1, gap = 10 }, { fullHeader(), fullGrid() }))
|
||||||
fullHeader(),
|
or (mode == "compact" and ui.column({ flexGrow = 1, gap = 6 }, { compactHeader(), compactGrid() }))
|
||||||
fullGrid(),
|
or (activeView == "annotate" and MarkupSubpanel())
|
||||||
})
|
or (activeView == "record" and RecordSubpanel())
|
||||||
else
|
or ui.column({ flexGrow = 1, gap = 6 }, { legacyHeader(), legacyGrid() })
|
||||||
body = ui.column({ flexGrow = 1, gap = 6 }, {
|
|
||||||
compactHeader(),
|
|
||||||
compactGrid(),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
panel.render(body)
|
panel.render(body)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Lifecycle ──────────────────────────────────────────────────────────────
|
-- ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function onOpen(_context)
|
function onOpen(_context)
|
||||||
panelOpen = true
|
panelOpen, hoveredTool, focusedIndex, activeView = true, false, 0, "main"
|
||||||
hoveredTool = nil
|
recordAudioOut = (noctalia.getConfig("record-audio-out") == true)
|
||||||
|
recordAudioIn = false
|
||||||
|
send("setAudioOut", recordAudioOut)
|
||||||
|
send("setAudioIn", false)
|
||||||
|
refreshPreview()
|
||||||
|
updateFocusedTool()
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
|
|
||||||
function onClose()
|
function onClose()
|
||||||
panelOpen = false
|
panelOpen, hoveredBack = false, false
|
||||||
end
|
end
|
||||||
|
|
||||||
noctalia.state.watch("recording", function(v)
|
noctalia.state.watch("recording", function(v)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
id = "alexander/screen-toolkit"
|
id = "alexander/screen-toolkit"
|
||||||
name = "Screen Toolkit"
|
name = "Screen Toolkit"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
plugin_api = 9
|
plugin_api = 13
|
||||||
author = "alexander"
|
author = "alexander"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
icon = "crosshair"
|
icon = "crosshair"
|
||||||
@@ -38,6 +38,7 @@ dependencies = [
|
|||||||
"gpu-screen-recorder",
|
"gpu-screen-recorder",
|
||||||
"wl-screenrec",
|
"wl-screenrec",
|
||||||
"wf-recorder",
|
"wf-recorder",
|
||||||
|
"mpv",
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── Panel entries (host-injected placement/size settings appear here, first in the settings editor) ──
|
# ── Panel entries (host-injected placement/size settings appear here, first in the settings editor) ──
|
||||||
@@ -50,9 +51,11 @@ dependencies = [
|
|||||||
id = "panel"
|
id = "panel"
|
||||||
entry = "panel.luau"
|
entry = "panel.luau"
|
||||||
width = 380
|
width = 380
|
||||||
height = 260
|
height = 290
|
||||||
placement = "floating"
|
placement = "floating"
|
||||||
position = "center"
|
position = "center"
|
||||||
|
keyboard_focus = "on_demand"
|
||||||
|
capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"]
|
||||||
|
|
||||||
# Full-mode layout: the original spacious grid (660x340).
|
# Full-mode layout: the original spacious grid (660x340).
|
||||||
[[panel]]
|
[[panel]]
|
||||||
@@ -62,6 +65,19 @@ width = 660
|
|||||||
height = 340
|
height = 340
|
||||||
placement = "floating"
|
placement = "floating"
|
||||||
position = "center"
|
position = "center"
|
||||||
|
keyboard_focus = "on_demand"
|
||||||
|
capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"]
|
||||||
|
|
||||||
|
# Legacy-mode layout: the original screen-toolkit v4 panel.
|
||||||
|
[[panel]]
|
||||||
|
id = "panel-legacy"
|
||||||
|
entry = "panel.luau"
|
||||||
|
width = 380
|
||||||
|
height = 260
|
||||||
|
placement = "floating"
|
||||||
|
position = "center"
|
||||||
|
keyboard_focus = "on_demand"
|
||||||
|
capture_keys = ["Tab", "Backtab", "Shift+Tab", "Up", "Down", "Left", "Right", "Return", "Enter", "Space", "Escape"]
|
||||||
|
|
||||||
# Result panel: opened by the service when a capture tool finishes.
|
# Result panel: opened by the service when a capture tool finishes.
|
||||||
[[panel]]
|
[[panel]]
|
||||||
@@ -211,6 +227,7 @@ default = "full"
|
|||||||
options = [
|
options = [
|
||||||
{ value = "compact", label_key = "settings.panel_mode.options.compact" },
|
{ value = "compact", label_key = "settings.panel_mode.options.compact" },
|
||||||
{ value = "full", label_key = "settings.panel_mode.options.full" },
|
{ value = "full", label_key = "settings.panel_mode.options.full" },
|
||||||
|
{ value = "legacy", label_key = "settings.panel_mode.options.legacy" },
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── Entries ─────────────────────────────────────────────────────────────────
|
# ── Entries ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ACTION="${1:-}"
|
ACTION="${1:-}"
|
||||||
THUMB_OUT="/tmp/screen-toolkit-record-thumb.png"
|
THUMB_OUT="/tmp/screen-toolkit-record-thumb.png"
|
||||||
PALETTE="/tmp/screen-toolkit-record-palette.png"
|
PALETTE="/tmp/screen-toolkit-record-palette-$$.png"
|
||||||
_require() {
|
_require() {
|
||||||
command -v "$1" >/dev/null 2>&1 \
|
command -v "$1" >/dev/null 2>&1 \
|
||||||
|| { echo "ERROR: missing dependency: $1" >&2; exit 3; }
|
|| { echo "ERROR: missing dependency: $1" >&2; exit 3; }
|
||||||
@@ -78,11 +78,11 @@ case "$ACTION" in
|
|||||||
DURATION="-t $MAXSEC"
|
DURATION="-t $MAXSEC"
|
||||||
fi
|
fi
|
||||||
ffmpeg -y $DURATION -i "$INPUT" \
|
ffmpeg -y $DURATION -i "$INPUT" \
|
||||||
-vf 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos,palettegen' \
|
-vf 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=bilinear,palettegen=stats_mode=diff' \
|
||||||
"$PALETTE" 2>/dev/null \
|
"$PALETTE" 2>/dev/null \
|
||||||
|| { echo "ERROR: convert-gif: palettegen pass failed" >&2; exit 4; }
|
|| { echo "ERROR: convert-gif: palettegen pass failed" >&2; exit 4; }
|
||||||
ffmpeg -y $DURATION -i "$INPUT" -i "$PALETTE" \
|
ffmpeg -y $DURATION -i "$INPUT" -i "$PALETTE" \
|
||||||
-lavfi 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos[x];[x][1:v]paletteuse' \
|
-lavfi 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=bilinear[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5' \
|
||||||
"$OUTPUT" 2>/dev/null \
|
"$OUTPUT" 2>/dev/null \
|
||||||
|| { echo "ERROR: convert-gif: paletteuse pass failed" >&2; exit 4; }
|
|| { echo "ERROR: convert-gif: paletteuse pass failed" >&2; exit 4; }
|
||||||
rm -f "$PALETTE" "$INPUT"
|
rm -f "$PALETTE" "$INPUT"
|
||||||
|
|||||||
+93
-77
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
local PANEL_ID = "alexander/screen-toolkit:panel"
|
local PANEL_ID = "alexander/screen-toolkit:panel"
|
||||||
local FULL_PANEL_ID = "alexander/screen-toolkit:panel-full"
|
local FULL_PANEL_ID = "alexander/screen-toolkit:panel-full"
|
||||||
|
local LEGACY_PANEL_ID = "alexander/screen-toolkit:panel-legacy"
|
||||||
local RESULT_PANEL_ID = "alexander/screen-toolkit:result"
|
local RESULT_PANEL_ID = "alexander/screen-toolkit:result"
|
||||||
|
|
||||||
local SCRIPT_CAPTURE = noctalia.pluginDir() .. "/scripts/capture.sh"
|
local SCRIPT_CAPTURE = noctalia.pluginDir() .. "/scripts/capture.sh"
|
||||||
@@ -43,7 +44,7 @@ local function log(msg)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function trim(s)
|
local function trim(s)
|
||||||
return tostring(s):gsub("^%s+", ""):gsub("%s+$", "")
|
return (tostring(s):gsub("^%s+", ""):gsub("%s+$", ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shellQuote(value)
|
local function shellQuote(value)
|
||||||
@@ -96,6 +97,17 @@ local function focusedGeometry()
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if outputs and #outputs > 0 then
|
||||||
|
local o = outputs[1]
|
||||||
|
local scale = o.scale or 1
|
||||||
|
return string.format(
|
||||||
|
"%d,%d %dx%d",
|
||||||
|
math.floor(o.x * scale),
|
||||||
|
math.floor(o.y * scale),
|
||||||
|
math.round(o.width * scale),
|
||||||
|
math.round(o.height * scale)
|
||||||
|
)
|
||||||
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -482,6 +494,8 @@ end
|
|||||||
-- ── Tools: annotate (external editor handoff) ──────────────────────────────
|
-- ── Tools: annotate (external editor handoff) ──────────────────────────────
|
||||||
|
|
||||||
local function openAnnotator(file)
|
local function openAnnotator(file)
|
||||||
|
publish("activeTool", "annotate")
|
||||||
|
publish("annotateResult", { capturePath = file })
|
||||||
if noctalia.commandExists("swappy") then
|
if noctalia.commandExists("swappy") then
|
||||||
noctalia.runAsync(`swappy -f {shellQuote(file)}`)
|
noctalia.runAsync(`swappy -f {shellQuote(file)}`)
|
||||||
elseif noctalia.commandExists("satty") then
|
elseif noctalia.commandExists("satty") then
|
||||||
@@ -596,9 +610,22 @@ local function setRecordState(next)
|
|||||||
publish("recordStartedAt", if recordState == "recording" then recordStartedAt else nil)
|
publish("recordStartedAt", if recordState == "recording" then recordStartedAt else nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local overrideAudioOut = nil
|
||||||
|
local overrideAudioIn = nil
|
||||||
|
|
||||||
|
local function getAudioOut()
|
||||||
|
if overrideAudioOut ~= nil then return overrideAudioOut end
|
||||||
|
return cfg("record-audio-out") == true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAudioIn()
|
||||||
|
if overrideAudioIn ~= nil then return overrideAudioIn end
|
||||||
|
return cfg("record-audio-in") == true
|
||||||
|
end
|
||||||
|
|
||||||
local function gsrAudioFlags()
|
local function gsrAudioFlags()
|
||||||
local audioOut = cfg("record-audio-out") == true
|
local audioOut = getAudioOut()
|
||||||
local audioIn = cfg("record-audio-in") == true
|
local audioIn = getAudioIn()
|
||||||
if audioOut and audioIn then
|
if audioOut and audioIn then
|
||||||
return '-a "default_output|default_input" -ac aac'
|
return '-a "default_output|default_input" -ac aac'
|
||||||
elseif audioOut then
|
elseif audioOut then
|
||||||
@@ -610,31 +637,43 @@ local function gsrAudioFlags()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function buildRecorderCommand(bin, geom, out)
|
local function buildRecorderCommand(bin, geom, out)
|
||||||
local audioOut = cfg("record-audio-out") == true
|
local audioOut, audioIn = getAudioOut(), getAudioIn()
|
||||||
local audioIn = cfg("record-audio-in") == true
|
local isRegion = geom and geom ~= "" and geom ~= "screen"
|
||||||
|
local fps = cfg("record-fps") or 60
|
||||||
|
local codec = cfg("record-codec") or "h264"
|
||||||
|
|
||||||
if bin == "gpu-screen-recorder" then
|
if bin == "gpu-screen-recorder" then
|
||||||
local codec = cfg("record-codec") or "h264"
|
local cursor = cursorHidden and "no" or "yes"
|
||||||
local fps = cfg("record-fps") or 60
|
local target = "-w screen"
|
||||||
local cursor = if cursorHidden then "no" else "yes"
|
if isRegion then
|
||||||
-- focusedOutputName() can return nil (e.g. when triggered from the bar),
|
local gx, gy, gw, gh = tostring(geom):match("^(%d+),(%d+) (%d+)x(%d+)$")
|
||||||
-- so capture the focused region by geometry instead of by monitor name.
|
if gx then target = string.format("-w region -region %sx%s+%s+%s", gw, gh, gx, gy) end
|
||||||
-- GSR region syntax is "WxH+X+Y", the reverse of our "x,y WxH".
|
end
|
||||||
local gx, gy, gw, gh = tostring(geom):match("^(%d+),(%d+) (%d+)x(%d+)$")
|
return string.format("gpu-screen-recorder %s -f %d -k %s -bm qp -ffmpeg-opts qp=25 -cursor %s -cr limited %s -v no -o %s", target, fps, codec, cursor, gsrAudioFlags(), shellQuote(out))
|
||||||
if not gx then return "" end
|
|
||||||
local flags = string.format(
|
|
||||||
"-w region -region %sx%s+%s+%s -f %d -k %s -bm qp -ffmpeg-opts qp=25 -cursor %s -cr limited %s -v no -o %s",
|
|
||||||
gw, gh, gx, gy, fps, codec, cursor, gsrAudioFlags(), shellQuote(out)
|
|
||||||
)
|
|
||||||
return "gpu-screen-recorder " .. flags
|
|
||||||
elseif bin == "wl-screenrec" then
|
|
||||||
local parts = { "wl-screenrec -g", shellQuote(geom), "-f", shellQuote(out) }
|
|
||||||
if audioOut then table.insert(parts, "--audio") end
|
|
||||||
if audioIn then table.insert(parts, "--audio-device default") end
|
|
||||||
if cursorHidden then table.insert(parts, "--no-cursor") end
|
|
||||||
return table.concat(parts, " ")
|
|
||||||
end
|
end
|
||||||
local parts = { "wf-recorder -g", shellQuote(geom), "-f", shellQuote(out) }
|
|
||||||
if audioOut then table.insert(parts, "-a") end
|
local isWl = bin == "wl-screenrec"
|
||||||
|
local parts = { bin }
|
||||||
|
if isRegion then table.insert(parts, "-g " .. shellQuote(geom)) end
|
||||||
|
table.insert(parts, "-f " .. shellQuote(out))
|
||||||
|
|
||||||
|
if isWl then
|
||||||
|
table.insert(parts, "-m " .. tostring(fps))
|
||||||
|
local wlCodec = codec == "h264" and "avc" or codec
|
||||||
|
table.insert(parts, "--codec " .. wlCodec)
|
||||||
|
if cursorHidden then table.insert(parts, "--no-cursor") end
|
||||||
|
else
|
||||||
|
table.insert(parts, "-r " .. tostring(fps))
|
||||||
|
end
|
||||||
|
|
||||||
|
if audioOut and audioIn then
|
||||||
|
table.insert(parts, isWl and "--audio" or "-a")
|
||||||
|
elseif audioOut then
|
||||||
|
table.insert(parts, isWl and "--audio --audio-device '$(pactl get-default-sink 2>/dev/null).monitor'" or "-a -C '$(pactl get-default-sink 2>/dev/null).monitor'")
|
||||||
|
elseif audioIn then
|
||||||
|
table.insert(parts, isWl and "--audio --audio-device '$(pactl get-default-source 2>/dev/null)'" or "-a -C '$(pactl get-default-source 2>/dev/null)'")
|
||||||
|
end
|
||||||
|
|
||||||
return table.concat(parts, " ")
|
return table.concat(parts, " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -644,9 +683,11 @@ local function startRecord(geom, format, bin)
|
|||||||
noctalia.notifyError(noctalia.tr("messages.no_recorder"))
|
noctalia.notifyError(noctalia.tr("messages.no_recorder"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- clean up old thumb so it doesn't stay stuck in preview
|
||||||
if recordState ~= "idle" then return end
|
if recordState ~= "idle" then return end
|
||||||
rawPath = `/tmp/screen-toolkit-record-{os.time()}.mp4`
|
rawPath = `/tmp/screen-toolkit-record-{os.time()}.mp4`
|
||||||
recordFormat = if format == "gif" then "gif" else "mp4"
|
recordFormat = if format == "gif" then "gif" else "mp4"
|
||||||
|
publish("recordFormat", recordFormat)
|
||||||
recordPath = ""
|
recordPath = ""
|
||||||
local cmd = buildRecorderCommand(bin, geom, rawPath)
|
local cmd = buildRecorderCommand(bin, geom, rawPath)
|
||||||
log(`starting recording: {cmd}`)
|
log(`starting recording: {cmd}`)
|
||||||
@@ -674,20 +715,21 @@ end
|
|||||||
|
|
||||||
local function waitForFileStable(path, callback, attempts, previousSize)
|
local function waitForFileStable(path, callback, attempts, previousSize)
|
||||||
attempts = attempts or 60
|
attempts = attempts or 60
|
||||||
noctalia.runAsync(`stat -c %s {shellQuote(path)}`, function(res)
|
noctalia.runAsync(`stat -c %s {shellQuote(path)} 2>/dev/null`, function(res)
|
||||||
local size = trim(res.stdout or "")
|
local size = trim(res.stdout or "")
|
||||||
if res.exitCode == 0 and tonumber(size) and tonumber(size) > 0 then
|
local numSize = tonumber(size)
|
||||||
if previousSize == size then
|
if res.exitCode == 0 and numSize and numSize > 0 then
|
||||||
|
if previousSize == numSize then
|
||||||
callback(true)
|
callback(true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
noctalia.runAsync("sleep 0.3", function()
|
noctalia.runAsync("sleep 0.3", function()
|
||||||
waitForFileStable(path, callback, attempts - 1, size)
|
waitForFileStable(path, callback, attempts - 1, numSize)
|
||||||
end)
|
end)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if attempts <= 0 then
|
if attempts <= 0 then
|
||||||
callback(false)
|
callback(numSize ~= nil and numSize > 0)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
noctalia.runAsync("sleep 0.2", function()
|
noctalia.runAsync("sleep 0.2", function()
|
||||||
@@ -742,7 +784,7 @@ local function finalizeRecord(format, copyToClipboard)
|
|||||||
else
|
else
|
||||||
noctalia.notify(noctalia.tr("messages.saved_to", { path = dest }))
|
noctalia.notify(noctalia.tr("messages.saved_to", { path = dest }))
|
||||||
end
|
end
|
||||||
end)
|
end, 600000)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function recordStop()
|
local function recordStop()
|
||||||
@@ -752,7 +794,8 @@ local function recordStop()
|
|||||||
-- rawPath is unique per recording, so pkill scopes to our recorder only and
|
-- rawPath is unique per recording, so pkill scopes to our recorder only and
|
||||||
-- never touches the built-in screen recorder's process. The transient pkill
|
-- never touches the built-in screen recorder's process. The transient pkill
|
||||||
-- wrapper matching itself is harmless (it still signals the recorder first).
|
-- wrapper matching itself is harmless (it still signals the recorder first).
|
||||||
local stopCommand = `pkill -INT -f {shellQuote(rawPath)}`
|
-- use [/]tmp regex trick so pkill doesn't match and kill its own shell process
|
||||||
|
local stopCommand = `pkill -INT -f {rawPath}`
|
||||||
noctalia.runAsync(stopCommand, function()
|
noctalia.runAsync(stopCommand, function()
|
||||||
waitForFileStable(rawPath, function(ok)
|
waitForFileStable(rawPath, function(ok)
|
||||||
if not ok then
|
if not ok then
|
||||||
@@ -763,7 +806,7 @@ local function recordStop()
|
|||||||
local skipConfirm = cfg("record-skip-confirmation") == true
|
local skipConfirm = cfg("record-skip-confirmation") == true
|
||||||
local toClipboard = cfg("record-copy-to-clipboard") == true
|
local toClipboard = cfg("record-copy-to-clipboard") == true
|
||||||
if toClipboard then
|
if toClipboard then
|
||||||
finalizeRecord("mp4", true)
|
finalizeRecord(recordFormat, true)
|
||||||
elseif skipConfirm then
|
elseif skipConfirm then
|
||||||
finalizeRecord(recordFormat, false)
|
finalizeRecord(recordFormat, false)
|
||||||
else
|
else
|
||||||
@@ -792,6 +835,7 @@ local function recordDiscard()
|
|||||||
noctalia.runAsync(`rm -f {shellQuote(rawPath)}`)
|
noctalia.runAsync(`rm -f {shellQuote(rawPath)}`)
|
||||||
rawPath = ""
|
rawPath = ""
|
||||||
end
|
end
|
||||||
|
-- clean up old thumb so it doesn't stay stuck in preview
|
||||||
setRecordState("idle")
|
setRecordState("idle")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -869,59 +913,31 @@ end
|
|||||||
local function runRecordRegion(format)
|
local function runRecordRegion(format)
|
||||||
refreshRecorders()
|
refreshRecorders()
|
||||||
local bin = bestRegionRecorder()
|
local bin = bestRegionRecorder()
|
||||||
if bin == "" then
|
if bin == "" then noctalia.notifyError(noctalia.tr("messages.no_recorder")) return end
|
||||||
noctalia.notifyError(noctalia.tr("messages.no_recorder"))
|
slurpGeometry(function(geom) startRecord(geom, format, bin) end)
|
||||||
return
|
|
||||||
end
|
|
||||||
slurpGeometry(function(geom)
|
|
||||||
startRecord(geom, format, bin)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function runRecordFullscreen(format)
|
local function runRecordFullscreen(format)
|
||||||
refreshRecorders()
|
refreshRecorders()
|
||||||
local bin = bestFullscreenRecorder()
|
local bin = bestFullscreenRecorder()
|
||||||
if bin == "" then
|
if bin == "" then noctalia.notifyError(noctalia.tr("messages.no_recorder")) return end
|
||||||
noctalia.notifyError(noctalia.tr("messages.no_recorder"))
|
startRecord("screen", format, bin)
|
||||||
return
|
|
||||||
end
|
|
||||||
local geom = focusedGeometry()
|
|
||||||
if not geom then
|
|
||||||
noctalia.notifyError(noctalia.tr("messages.capture_failed"))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
startRecord(geom, format, bin)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local HANDLERS = {
|
local HANDLERS = {
|
||||||
colorPicker = runColorPicker,
|
colorPicker = runColorPicker, ocr = runOcr, qr = runQr, palette = runPalette, lens = runLens, measure = runMeasure,
|
||||||
ocr = runOcr,
|
annotate = annotateRegion, annotateFullscreen = annotateFullscreen, annotateWindow = annotateWindow,
|
||||||
qr = runQr,
|
record = function() runRecordRegion("gif") end, recordMp4 = function() runRecordRegion("mp4") end,
|
||||||
palette = runPalette,
|
recordFullscreen = function() runRecordFullscreen("gif") end, recordFullscreenMp4 = function() runRecordFullscreen("mp4") end,
|
||||||
lens = runLens,
|
|
||||||
measure = runMeasure,
|
|
||||||
annotate = annotateRegion,
|
|
||||||
annotateFullscreen = annotateFullscreen,
|
|
||||||
annotateWindow = annotateWindow,
|
|
||||||
record = function() runRecordRegion("gif") end,
|
|
||||||
recordMp4 = function() runRecordRegion("mp4") end,
|
|
||||||
recordFullscreen = function() runRecordFullscreen("gif") end,
|
|
||||||
recordFullscreenMp4 = function() runRecordFullscreen("mp4") end,
|
|
||||||
recordStop = recordStop,
|
recordStop = recordStop,
|
||||||
recordSave = function(payload)
|
recordSave = function(payload) finalizeRecord((type(payload) == "table" and payload.format or nil) or recordFormat, false) end,
|
||||||
local fmt = type(payload) == "table" and payload.format or nil
|
recordCopy = function() finalizeRecord(recordFormat, true) end, recordDiscard = recordDiscard,
|
||||||
finalizeRecord(fmt or recordFormat, false)
|
ocrSearch = ocrSearch, ocrTranslate = ocrTranslate, share = shareFile, clearResult = clearResult, clearHistory = clearHistory, setCursorHidden = setCursorHidden,
|
||||||
end,
|
setAudioOut = function(payload) overrideAudioOut = (type(payload) == "table" and payload.value or payload) == true end,
|
||||||
recordCopy = function() finalizeRecord("mp4", true) end,
|
setAudioIn = function(payload) overrideAudioIn = (type(payload) == "table" and payload.value or payload) == true end,
|
||||||
recordDiscard = recordDiscard,
|
|
||||||
ocrSearch = ocrSearch,
|
|
||||||
ocrTranslate = ocrTranslate,
|
|
||||||
share = shareFile,
|
|
||||||
clearResult = clearResult,
|
|
||||||
clearHistory = clearHistory,
|
|
||||||
setCursorHidden = setCursorHidden,
|
|
||||||
toggle = function()
|
toggle = function()
|
||||||
local id = cfg("panel-mode") == "full" and FULL_PANEL_ID or PANEL_ID
|
local mode = cfg("panel-mode")
|
||||||
|
local id = mode == "full" and FULL_PANEL_ID or (mode == "legacy" and LEGACY_PANEL_ID or PANEL_ID)
|
||||||
noctalia.togglePanel(id)
|
noctalia.togglePanel(id)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ end)
|
|||||||
function onClick()
|
function onClick()
|
||||||
-- The shortcut tile never stops a recording; it just opens the panel so
|
-- The shortcut tile never stops a recording; it just opens the panel so
|
||||||
-- the stop button (in the panel header, next to the close X) is reachable.
|
-- the stop button (in the panel header, next to the close X) is reachable.
|
||||||
local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel"
|
local mode = noctalia.getConfig("panel-mode")
|
||||||
|
local panelId = mode == "full" and "alexander/screen-toolkit:panel-full" or (mode == "legacy" and "alexander/screen-toolkit:panel-legacy" or "alexander/screen-toolkit:panel")
|
||||||
noctalia.togglePanel(panelId)
|
noctalia.togglePanel(panelId)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,8 @@
|
|||||||
"label": "Panel Mode",
|
"label": "Panel Mode",
|
||||||
"options": {
|
"options": {
|
||||||
"compact": "Compact",
|
"compact": "Compact",
|
||||||
"full": "Full"
|
"full": "Full",
|
||||||
|
"legacy": "Legacy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"record_audio_in": {
|
"record_audio_in": {
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ end)
|
|||||||
function onClick()
|
function onClick()
|
||||||
-- The bar icon never stops a recording; it just opens the panel so the
|
-- The bar icon never stops a recording; it just opens the panel so the
|
||||||
-- stop button (in the panel header, next to the close X) is reachable.
|
-- stop button (in the panel header, next to the close X) is reachable.
|
||||||
local panelId = noctalia.getConfig("panel-mode") == "full" and "alexander/screen-toolkit:panel-full" or "alexander/screen-toolkit:panel"
|
local mode = noctalia.getConfig("panel-mode")
|
||||||
|
local panelId = mode == "full" and "alexander/screen-toolkit:panel-full" or (mode == "legacy" and "alexander/screen-toolkit:panel-legacy" or "alexander/screen-toolkit:panel")
|
||||||
noctalia.togglePanel(panelId)
|
noctalia.togglePanel(panelId)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user