Skip to content

Commit 8b9424f

Browse files
committed
ReaDashboard v1.1.1
1 parent 21330f8 commit 8b9424f

1 file changed

Lines changed: 128 additions & 25 deletions

File tree

ReaDashboard/Anshul_ReaDashboard.lua

Lines changed: 128 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- @description ReaDashboard
2-
-- @version 1.1.0
2+
-- @version 1.1.1
33
-- @author Anshul
44
-- @credits solger (for ReaLauncher concept)
55
-- @about
@@ -11,6 +11,11 @@
1111
-- - SWS Extensions
1212
-- @changelog
1313
--
14+
-- v1.1.1
15+
-- + Added recursive subfolder scanning for artwork with configurable depth (Settings -> Data Sources)
16+
-- + Added Tab key shortcut to jump focus from project area back to the search bar
17+
-- + Added Ctrl+Down/Up shortcuts to instantly jump focus from the search bar into the project area
18+
--
1419
-- v1.1.0
1520
-- + Guitar Mode toggle — hide all guitar-specific fields (strings, tuning, transpose, guitar, amp) via Settings; data untouched
1621
-- + Scan staleness indicator — status bar shows when the last hard refresh was run (right-aligned, faint); Refresh button tooltip updated
@@ -142,7 +147,7 @@ local ImGui = require 'imgui' '0.10'
142147
local CFG = {
143148
-- Script identity
144149
SCRIPT_NAME = 'ReaDashboard',
145-
SCRIPT_VERSION = '1.1.0',
150+
SCRIPT_VERSION = '1.1.1',
146151
EXT_SECTION = 'ReaDashboard',
147152

148153
-- Window defaults
@@ -549,6 +554,7 @@ local S = {
549554
active_tab = 'recent', -- 'recent', 'all', or 'settings'
550555
universal_search = true,
551556
auto_focus_search = false, -- auto-focus search bar on script open
557+
search_bar_active = false, -- runtime: search bar has focus or was just deactivated this frame
552558
recent_count_in_filtered = 0,
553559
keep_open = true,
554560
persistent_mode = false, -- keep script alive in background when window closed
@@ -654,6 +660,7 @@ local S = {
654660
ALL_PROJECTS_PATH = '', -- primary path (legacy, for backward compat)
655661
additional_project_paths = {}, -- additional scan paths (list of strings)
656662
default_artwork_path = '', -- fallback art when project has no art
663+
art_scan_depth = 1, -- depth to recursively scan for artwork (0 = root only)
657664
placeholder_full_name = false, -- show full project name on placeholder instead of initials
658665
debug_logging = false,
659666
ALL_SCAN_MAX_DEPTH = 10,
@@ -718,14 +725,15 @@ local S = {
718725

719726
-- Search history
720727
search_history = {}, -- circular buffer of recent searches (newest first)
721-
search_history_max = 8, -- max entries to keep
728+
search_history_max = 10, -- max entries to keep
722729
search_history_enabled = true, -- toggle in Settings
723730
search_history_idx = 0, -- current Up/Down arrow index (0 = live input, 1+ = history)
724731
search_buf_live = '', -- live input before arrow navigation started
725732
search_history_callback = nil, -- EEL callback function for InputTextFlags_CallbackHistory
726733

727734
-- Programmatic tab switching (set by keyboard shortcut, consumed by tab drawing code)
728735
pending_tab = nil, -- nil or 'recent'/'all'/'settings'/'actions'
736+
focus_search_pending = false, -- runtime: Ctrl+F triggered, focus search bar next frame
729737
}
730738

731739
--- Recompute all derived theme colors from current S state. Call after any theme setting change.
@@ -2636,6 +2644,37 @@ local function LoadRecentProjects()
26362644
return result
26372645
end
26382646

2647+
--- Helper: Collect image full paths recursively up to max_depth.
2648+
local function CollectImages(dir, max_depth, current_depth, results)
2649+
current_depth = current_depth or 0
2650+
results = results or {}
2651+
2652+
local fidx = 0
2653+
while true do
2654+
local fname = reaper.EnumerateFiles(dir, fidx)
2655+
if not fname then break end
2656+
local ext = fname:lower():match('%.([^%.]+)$')
2657+
if ext == 'jpg' or ext == 'jpeg' or ext == 'png' then
2658+
results[#results + 1] = dir .. '/' .. fname
2659+
end
2660+
fidx = fidx + 1
2661+
end
2662+
2663+
if current_depth < max_depth then
2664+
local didx = 0
2665+
while true do
2666+
local dname = reaper.EnumerateSubdirectories(dir, didx)
2667+
if not dname then break end
2668+
-- ignore dot folders (.git, .reaper, etc) to be safe
2669+
if dname:sub(1, 1) ~= '.' then
2670+
CollectImages(dir .. '/' .. dname, max_depth, current_depth + 1, results)
2671+
end
2672+
didx = didx + 1
2673+
end
2674+
end
2675+
return results
2676+
end
2677+
26392678
--- Enrich projects with metadata from spicetify DB, RPP parsing, album art, and tags.
26402679
local function EnrichProjects(projectList)
26412680
ScanSpicetifyDB()
@@ -2688,30 +2727,21 @@ local function EnrichProjects(projectList)
26882727
end
26892728
end
26902729

2691-
-- 1b-1d. Relaxed matching: enumerate all images in folder
2730+
-- 1b-1d. Relaxed matching: enumerate all images in folder and allowed subfolders
26922731
if not artFound then
2693-
local images = {}
2694-
local fidx = 0
2695-
while true do
2696-
local fname = reaper.EnumerateFiles(proj.dir, fidx)
2697-
if not fname then break end
2698-
local ext = fname:lower():match('%.([^%.]+)$')
2699-
if ext == 'jpg' or ext == 'jpeg' or ext == 'png' then
2700-
images[#images + 1] = fname
2701-
end
2702-
fidx = fidx + 1
2703-
end
2732+
local images = CollectImages(proj.dir, S.art_scan_depth)
27042733

27052734
if #images == 1 then
2706-
-- 1b. Single image rule: only one image in folder → use it
2707-
proj.albumArtPath = proj.dir .. '/' .. images[1]
2735+
-- 1b. Single image rule: only one image found → use it
2736+
proj.albumArtPath = images[1]
27082737
artFound = true
27092738
elseif #images > 1 then
27102739
-- 1c. Project-name match: image filename contains the project name
27112740
local projNameLower = proj.name:lower()
2712-
for _, img in ipairs(images) do
2713-
if img:lower():find(projNameLower, 1, true) then
2714-
proj.albumArtPath = proj.dir .. '/' .. img
2741+
for _, imgPath in ipairs(images) do
2742+
local filename = imgPath:match('[^/\\]+$') or imgPath
2743+
if filename:lower():find(projNameLower, 1, true) then
2744+
proj.albumArtPath = imgPath
27152745
artFound = true
27162746
break
27172747
end
@@ -2720,8 +2750,7 @@ local function EnrichProjects(projectList)
27202750
-- 1d. Most-recently-modified fallback
27212751
if not artFound then
27222752
local bestImg, bestDate = nil, ''
2723-
for _, img in ipairs(images) do
2724-
local imgPath = proj.dir .. '/' .. img
2753+
for _, imgPath in ipairs(images) do
27252754
local imgExists, imgDate = GetFileInfo(imgPath)
27262755
if imgExists and imgDate > bestDate then
27272756
bestDate = imgDate
@@ -3911,6 +3940,7 @@ local function SaveState()
39113940
Set('symlink_dest', S.symlink_dest)
39123941

39133942
Set('default_artwork_path', S.default_artwork_path)
3943+
Set('art_scan_depth', tostring(S.art_scan_depth))
39143944
Set('placeholder_full_name', S.placeholder_full_name and '1' or '0')
39153945
Set('all_projects_path', S.ALL_PROJECTS_PATH)
39163946
Set('additional_project_paths', table.concat(S.additional_project_paths, '|'))
@@ -4136,6 +4166,8 @@ local function LoadState()
41364166

41374167
local dap = G('default_artwork_path')
41384168
if dap ~= '' then S.default_artwork_path = dap end
4169+
local asd = tonumber(G('art_scan_depth'))
4170+
if asd then S.art_scan_depth = asd end
41394171
local pfn = G('placeholder_full_name')
41404172
if pfn ~= '' then S.placeholder_full_name = (pfn == '1') end
41414173

@@ -4439,6 +4471,12 @@ local function DrawTopBar()
44394471
ImGui.SetKeyboardFocusHere(ctx)
44404472
end
44414473

4474+
-- Ctrl+F: one-shot focus request
4475+
if S.focus_search_pending then
4476+
ImGui.SetKeyboardFocusHere(ctx)
4477+
S.focus_search_pending = false
4478+
end
4479+
44424480
-- Before rendering InputText: reset callback signals and pre-compute both
44434481
-- possible history buffers so the EEL callback can pick the right one.
44444482
-- Direction: Down = older history, Up = newer/live (reversed from typical shell).
@@ -4477,8 +4515,24 @@ local function DrawTopBar()
44774515
local use_history_cb = S.search_history_enabled and S.search_history_callback and #S.search_history > 0
44784516
local search_flags = use_history_cb and ImGui.InputTextFlags_CallbackHistory or 0
44794517
local search_cb = use_history_cb and S.search_history_callback or nil
4518+
if use_history_cb then
4519+
local ctrl_down = ImGui.IsKeyDown(ctx, ImGui.Key_LeftCtrl) or ImGui.IsKeyDown(ctx, ImGui.Key_RightCtrl)
4520+
ImGui.Function_SetValue(S.search_history_callback, 'ctrl_is_down', ctrl_down and 1 or 0)
4521+
end
44804522
changed, S.search_buf = ImGui.InputTextWithHint(ctx, '##search', 'Search projects, artists, tags...', S.search_buf, search_flags, search_cb)
44814523
local search_deactivated = ImGui.IsItemDeactivated(ctx)
4524+
-- Capture while we're still on the right item: true if currently editing OR just deactivated this frame.
4525+
-- Used by HandleKeys to avoid closing the window when Escape is pressed to exit the search bar.
4526+
S.search_bar_active = ImGui.IsItemActive(ctx) or search_deactivated
4527+
4528+
-- Tab while search bar is focused: jump directly to project list
4529+
if S.search_bar_active and ImGui.IsKeyPressed(ctx, ImGui.Key_Tab) then
4530+
local shift_held = ImGui.IsKeyDown(ctx, ImGui.Key_LeftShift) or ImGui.IsKeyDown(ctx, ImGui.Key_RightShift)
4531+
if not shift_held then
4532+
local new_idx = S.selected_idx < 1 and 1 or S.selected_idx
4533+
SelectOnly(new_idx)
4534+
end
4535+
end
44824536

44834537
-- Auto-save to history when search bar loses focus with non-empty text
44844538
if search_deactivated and S.search_buf ~= '' then
@@ -6981,6 +7035,11 @@ local function HandleKeys()
69817035
return
69827036
end
69837037

7038+
-- Ctrl+F = focus search bar (works even when other items have focus)
7039+
if ctrl_down and ImGui.IsKeyPressed(ctx, ImGui.Key_F) then
7040+
S.focus_search_pending = true
7041+
end
7042+
69847043
-- Ctrl+1/2/3/4 = switch to tab (Recent/All/Settings/Actions)
69857044
if ctrl_down then
69867045
local tab_targets = { 'recent', 'all', 'settings', 'actions' }
@@ -7005,6 +7064,29 @@ local function HandleKeys()
70057064
end
70067065
end
70077066

7067+
-- Ctrl+Down/Up: Jump focus from search bar to project area
7068+
-- If already in project area, this falls through to normal arrow navigation
7069+
if ctrl_down and (ImGui.IsKeyPressed(ctx, ImGui.Key_DownArrow) or ImGui.IsKeyPressed(ctx, ImGui.Key_UpArrow)) then
7070+
if S.search_bar_active then
7071+
-- Search -> Project area
7072+
local new_idx = S.selected_idx < 1 and 1 or S.selected_idx
7073+
SelectOnly(new_idx)
7074+
return -- Prevent Arrow Key navigation block from processing this as a double-movement
7075+
end
7076+
end
7077+
7078+
-- Tab: Return focus to search bar from project area
7079+
if ImGui.IsKeyPressed(ctx, ImGui.Key_Tab) and not S.search_bar_active then
7080+
local shift_held = ImGui.IsKeyDown(ctx, ImGui.Key_LeftShift) or ImGui.IsKeyDown(ctx, ImGui.Key_RightShift)
7081+
if not shift_held and not ctrl_down then
7082+
-- Only jump if we are in the project tabs, so we don't break Settings tab navigation
7083+
if S.active_tab == 'recent' or S.active_tab == 'all' or S.active_tab == 'favorites' then
7084+
S.focus_search_pending = true
7085+
return
7086+
end
7087+
end
7088+
end
7089+
70087090
-- F5 = normal refresh (cached), Shift+F5 = hard refresh (re-scan all)
70097091
if ImGui.IsKeyPressed(ctx, ImGui.Key_F5) then
70107092
if ImGui.IsKeyDown(ctx, ImGui.Key_LeftShift) or ImGui.IsKeyDown(ctx, ImGui.Key_RightShift) then
@@ -7023,13 +7105,15 @@ local function HandleKeys()
70237105
S.window_open = false
70247106
end
70257107

7026-
-- Escape: clear selection first, then close/hide window if setting allows
7108+
-- Escape: clear selection first, then close/hide window if setting allows.
7109+
-- Guard: if the search bar was active this frame, Escape only unfocused it (Dear ImGui
7110+
-- handles that internally). Do NOT also close the window — let the user start navigating.
70277111
if ImGui.IsKeyPressed(ctx, ImGui.Key_Escape) and not shift_held then
70287112
if SelectionCount() > 1 then
70297113
-- Reduce to single selection on primary idx
70307114
S.selected = {}
70317115
if S.selected_idx > 0 then S.selected[S.selected_idx] = true end
7032-
elseif S.close_on_escape then
7116+
elseif S.close_on_escape and not S.search_bar_active then
70337117
S.window_open = false
70347118
end
70357119
end
@@ -7122,6 +7206,11 @@ local function HandleKeys()
71227206
end
71237207
end
71247208
end
7209+
7210+
-- / = focus search bar (only when no input is active, to avoid stealing the character)
7211+
if ImGui.IsKeyPressed(ctx, ImGui.Key_Slash) and not ctrl_down then
7212+
S.focus_search_pending = true
7213+
end
71257214
end
71267215
end
71277216

@@ -7268,6 +7357,15 @@ local function DrawSettingsTab()
72687357
end
72697358
end
72707359

7360+
-- Artwork Recursive Scan Depth
7361+
ImGui.AlignTextToFramePadding(ctx)
7362+
ImGui.Text(ctx, 'Art Scan Depth:')
7363+
if ImGui.IsItemHovered(ctx) then ImGui.SetTooltip(ctx, 'Subfolder depth to scan for custom artwork (1 = immediate subfolders).\n0 = only check root project folder.') end
7364+
ImGui.SameLine(ctx, lbl_w)
7365+
ImGui.SetNextItemWidth(ctx, 100)
7366+
local sd_chg, sd_new = ImGui.InputInt(ctx, '##art_scan_depth', S.art_scan_depth)
7367+
if sd_chg then S.art_scan_depth = math.max(0, math.min(9, sd_new)); changed = true end
7368+
72717369
ImGui.Spacing(ctx)
72727370

72737371
-- Guitar mode toggle
@@ -8459,6 +8557,11 @@ local function DrawActionsTab()
84598557
{ 'Up / Down', 'Navigate project list (in grid: move by row) or cycle search history' },
84608558
{ 'Left / Right', 'Navigate grid columns' },
84618559
{ 'Home / End', 'Jump to first or last project' },
8560+
{ 'Tab', 'Toggle focus between search bar and project list' },
8561+
{ 'Ctrl+Down/Up', 'Jump focus from search bar to project list' },
8562+
{ 'Ctrl+F or /', 'Focus search bar' },
8563+
{ 'Escape (in search)', 'Unfocus search bar (does not close window)' },
8564+
{ 'Enter (in search)', 'Open selected / first result directly' },
84628565
{ 'Ctrl+A', 'Select all visible projects' },
84638566
{ 'Ctrl+C', 'Copy selected project path(s)' },
84648567
{ 'Ctrl+Click', 'Toggle project in/out of selection' },
@@ -9061,7 +9164,7 @@ local function Init()
90619164
-- Arrow key mapping: Down = older history (direction 1), Up = newer/live (direction -1).
90629165
-- This feels natural: "scroll down through past searches, up to return to current".
90639166
S.search_history_callback = ImGui.CreateFunctionFromEEL([[
9064-
EventFlag == InputTextFlags_CallbackHistory ? (
9167+
EventFlag == InputTextFlags_CallbackHistory && !ctrl_is_down ? (
90659168
EventKey == Key_DownArrow ? (
90669169
can_go_older ? (
90679170
hist_direction = 1;

0 commit comments

Comments
 (0)