From e1f7209131cafab74f0665599a13d668ebeb410c Mon Sep 17 00:00:00 2001 From: David Vazgenovich Shakaryan Date: Fri, 6 Feb 2026 12:30:32 -0800 Subject: implement global EPG search --- main.lua | 1 + osd.lua | 40 +++------------------ rt.lua | 121 ++++++++++++++++++++++++++++++++++---------------------------- rx.lua | 59 ++++++++++++++++++++++++++++++ state.lua | 36 +++++++++++++++++++ 5 files changed, 168 insertions(+), 89 deletions(-) diff --git a/main.lua b/main.lua index 1224ec2..67a195d 100644 --- a/main.lua +++ b/main.lua @@ -129,6 +129,7 @@ input.set_persistent_mapping({ input.define_mapping('MENU', { ['BS'] = {rt.prev_menu}, ['/'] = {rt.start_search}, + ['E'] = {rt.start_epg_search}, ['Ctrl+s'] = {rt.toggle_menu_sort}, ['Ctrl+r'] = {rt.reload_option}, ['Ctrl+R'] = {rt.reload_data}, diff --git a/osd.lua b/osd.lua index 2152863..0e06f22 100644 --- a/osd.lua +++ b/osd.lua @@ -155,44 +155,14 @@ function mt:menu_lines(state) end function mt:menu_title(menu) - local str = asscape(menu.title) + local str, has_tags = menu:get_title() + str = asscape(str) local col = menu.search_active and colour.selected or colour.title - if menu.type == 'search' then - local start = #menu.search_text - #menu.search_term - local prefix = menu.search_text:sub(1, start) - local term = menu.search_text:sub(start + 1) - - if str:find('', 1, true) then - str = str:gsub('', function() - return colour.search_prefix .. - asscape(prefix) .. col .. asscape(term) - end) - end - - if str:find('', 1, true) then - local idx = menu.search_cursor - if idx <= start then - prefix = asscape(prefix:sub(1, idx - 1)) .. - col .. cursor_glyph .. - colour.search_prefix .. - asscape(prefix:sub(idx)) - term = asscape(term) - else - idx = idx - start - prefix = asscape(prefix) - term = asscape(term:sub(1, idx - 1)) .. - cursor_glyph .. asscape(term:sub(idx)) - end - str = str:gsub('', function() - return colour.search_prefix .. prefix .. col .. - term - end) - end - + if has_tags then + str = str:gsub('', cursor_glyph) str = str:gsub('', colour) - str = str:gsub('', #menu.options) - str = str:gsub('', #menu.search_options) + str = str:gsub('', col) end if menu:is_sorted() then diff --git a/rt.lua b/rt.lua index 8ed3dc9..4a90e6e 100644 --- a/rt.lua +++ b/rt.lua @@ -482,49 +482,6 @@ function rt.favourite_option() osd:dirty() end -function rt.goto_option() - local menu = state:menu() - local opt = menu.options[menu.cursor] - if not opt or not opt.path then - return - end - - if menu.group_id == 'favourites' then - state.depth = 1 - elseif menu.type == 'search' then - state.depth = state.depth - 1 - if state:menu().group_id == 'favourites' then - merge_refreshed_menu_options() - end - end - - for i = 1, #opt.path do - cursor_to_id(opt.path[i].id) - rt.push_group_menu(opt.path[i]) - end - cursor_to_id(opt.id) - - osd:dirty() -end - -function rt.goto_playing() - local id = state.playing_id - local obj = id and ctx.catalogue:get(id) - local path = obj and ctx.catalogue:path_to_root(obj) - if not path then - return - end - - state.depth = 1 - for i = #path, 1, -1 do - cursor_to_id(path[i].id) - rt.push_group_menu(path[i]) - end - cursor_to_id(id) - - osd:dirty() -end - local function open_option_programme_info(opt, img_url) local prog = opt.programme local options = rx.menu_options_programme_info(prog) @@ -621,6 +578,53 @@ function rt.open_option_info(opt) end end +function rt.goto_option() + local menu = state:menu() + local opt = menu.options[menu.cursor] + if not opt or not opt.path then + return + end + + if menu.group_id == 'favourites' then + state.depth = 1 + elseif menu.type == 'search' then + state.depth = state.depth - 1 + if state:menu().group_id == 'favourites' then + merge_refreshed_menu_options() + end + end + + for i = 1, #opt.path do + cursor_to_id(opt.path[i].id) + if i == #opt.path and opt.type == 'programme' then + open_option_channel_epg(opt.path[i]) + else + rt.push_group_menu(opt.path[i]) + end + end + cursor_to_id(opt.id) + + osd:dirty() +end + +function rt.goto_playing() + local id = state.playing_id + local obj = id and ctx.catalogue:get(id) + local path = obj and ctx.catalogue:path_to_root(obj) + if not path then + return + end + + state.depth = 1 + for i = #path, 1, -1 do + cursor_to_id(path[i].id) + rt.push_group_menu(path[i]) + end + cursor_to_id(id) + + osd:dirty() +end + local F_NAME = {name = true} local F_INFO = {info = true} local F_BOTH = {name = true, info = true} @@ -739,14 +743,12 @@ function rt.search_cursor_end() search_cursor_move(function(str) return #str + 1 end) end -function rt.start_search() +function rt.start_search(options, search_type, title) local menu = state:menu() - local title = 'Searching: ' .. - ' (/)' - - if menu.type == 'search' then + if menu.type == 'search' and + (not search_type or + menu.search_type == search_type) then menu:save_checkpoint() - menu.title = title menu.search_active = true menu:set_search_cursor(#menu.search_text + 1) menu:set_cursor(1) @@ -754,8 +756,10 @@ function rt.start_search() state:push_menu({ title = title, type = 'search', - options = rx.menu_options_search(menu.options), + options = options or + rx.menu_options_search(menu.options), search_active = true, + search_type = search_type, }) end @@ -763,11 +767,20 @@ function rt.start_search() input.set_mapping('SEARCH') end -function rt.end_search() +function rt.start_epg_search() local menu = state:menu() - menu.search_active = false - menu.title = 'Search results: ' .. - ' (/)' + local options = menu.search_type ~= 'epg' and + rx.menu_options_epg_search(menu.options) or nil + if options and not next(options) then + osd:flash_error('No EPG data found') + return + end + + rt.start_search(options, 'epg', 'EPG') +end + +function rt.end_search() + state:menu().search_active = false osd:dirty() input.set_mapping('MENU') end diff --git a/rx.lua b/rx.lua index d96420f..0c85029 100644 --- a/rx.lua +++ b/rx.lua @@ -281,6 +281,7 @@ function rx.menu_options_channel_epg(src_id, ch) local time = os.time() for i, v in ipairs(progs) do local prog = { + id = ch .. ':' .. i, name = os.date('%a %d %b %H:%M', v.start) .. ' ' .. os.date('%H:%M', v.stop) .. ' ' .. v.title, type = 'programme', @@ -315,6 +316,64 @@ function rx.menu_options_programme_info(prog) return options end +local function menu_options_epg_search_build_progs(options, opt, path, time) + local ch = opt.epg_channel_id + local progs = ctx.src[opt.src_id].epg:channel_programmes(ch) + if not progs then + return + end + + for i, v in ipairs(progs) do + if time < v.stop then + options[#options+1] = setmetatable({ + id = ch .. ':' .. i, + name = os.date('%a %d %b %H:%M', v.start) .. + ' ' .. os.date('%H:%M', v.stop) .. + ' ' .. v.title, + type = 'programme', + programme = v, + path = path, + }, programme_mt) + end + end +end + +local function menu_options_epg_search_build(options, opts, path, seen, time) + for _, v in ipairs(opts) do + if v.id and not seen[v.id] then + seen[v.id] = true + path[#path+1] = v + if v.epg_channel_id then + menu_options_epg_search_build_progs( + options, v, util.copy_table(path), + time) + elseif v.type == 'group' and v.children then + menu_options_epg_search_build( + options, v.children, path, seen, time) + end + path[#path] = nil + end + end +end + +function rx.menu_options_epg_search(opts) + local options = {} + local time = os.time() + + menu_options_epg_search_build(options, opts, {}, {}, time) + table.sort(options, function(a, b) + if a.programme.start ~= b.programme.start then + return a.programme.start < b.programme.start + end + if a.programme.title ~= b.programme.title then + return a.programme.title < b.programme.title + end + return a.path[#a.path].name < b.path[#b.path].name + end) + + return options +end + local function menu_options_search_build(options, t, path) for _, v in ipairs(options) do v.path = path diff --git a/state.lua b/state.lua index 4db4837..4d97fe3 100644 --- a/state.lua +++ b/state.lua @@ -195,6 +195,42 @@ function menu_mt:set_options(options) self:set_cursor(1) end +function menu_mt:get_title() + if self.type == 'search' then + local str = self.search_active and 'Searching: ' or + 'Search results: ' + if self.title then + str = '(' .. self.title .. ') ' .. str + end + + local start = #self.search_text - #self.search_term + local prefix = self.search_text:sub(1, start) + local term = self.search_text:sub(start + 1) + if self.search_active then + local idx = self.search_cursor + if idx <= start then + prefix = prefix:sub(1, idx - 1) .. + '' .. + '' .. + prefix:sub(idx) + else + idx = idx - start + term = term:sub(1, idx - 1) .. '' .. + term:sub(idx) + end + end + str = str .. '' .. prefix .. + '' .. term + + str = str .. ' (' .. #self.options .. '/' .. + #self.search_options .. ')' + + return str, true + end + + return self.title +end + function menu_mt:set_search_cursor(pos) local pos = math.max(1, math.min(#self.search_text + 1, pos)) if pos == self.search_cursor then -- cgit v1.2.3-70-g09d2