summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-07 15:22:39 -0800
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-07 15:22:39 -0800
commit2dc4809f5cba14838def5abea3220793ae0d8e4a (patch)
treea0a2b746e37d99413e3d3d033553d15ce0bf6529
parentc442abc24ebbddaf8d9fab36375128e06547d65e (diff)
downloadmpv-iptv-menu-2dc4809f5cba14838def5abea3220793ae0d8e4a.tar.gz
mpv-iptv-menu-2dc4809f5cba14838def5abea3220793ae0d8e4a.tar.xz
move generic menu operations to metatable
-rw-r--r--main.lua139
-rw-r--r--state.lua114
2 files changed, 135 insertions, 118 deletions
diff --git a/main.lua b/main.lua
index 524d727..57a4e12 100644
--- a/main.lua
+++ b/main.lua
@@ -101,28 +101,7 @@ local function save_favourites()
end
local function set_cursor(pos, opts)
- local menu = state:menu()
- local lines = osd_menu_lines()
-
- local pos = math.max(1, math.min(pos, #menu.options))
- local top = menu.view_top
- if opts and opts.centre then
- top = pos - math.floor((osd_menu_lines() - 1) / 2)
- elseif opts and opts.keep_offset then
- top = top + pos - menu.cursor
- end
-
- -- move view to keep selected option visible
- if pos < top then
- top = pos
- elseif pos > top + lines - 1 then
- top = pos - lines + 1
- end
-
- top = math.max(1, math.min(top, #menu.options - lines + 1))
-
- menu.cursor = pos
- menu.view_top = top
+ state:menu():set_cursor(pos, osd_menu_lines(), opts)
if not (opts and opts.skip_redraw) then
update_osd()
@@ -408,29 +387,6 @@ local function push_group_menu(group)
update_osd()
end
-local update_search_matches
-
-local function set_menu_sort(menu, bool)
- if not menu.sorted == not bool then
- return
- end
-
- local key = menu.type == 'search' and 'search_options' or 'options'
- if bool then
- menu['orig_' .. key] = menu[key]
- menu[key] = util.copy_table(menu[key])
- sort_options(menu[key])
- else
- menu[key] = menu['orig_' .. key]
- menu['orig_' .. key] = nil
- end
-
- if menu.type == 'search' then
- update_search_matches()
- end
- menu.sorted = bool
-end
-
-- refresh options when navigating up the stack to a previous favourites menu.
-- existing menu options are never removed, even if unfavourited. the new order
-- is always respected, while still preserving the relative order of existing
@@ -440,7 +396,7 @@ local function refresh_favourites_menu()
local opt = menu.options[menu.cursor]
local sorted = menu.sorted
if sorted then
- set_menu_sort(menu, false)
+ menu.set_sort(false)
end
local options = group_menu_options(catalogue:get(menu.group_id))
@@ -474,7 +430,7 @@ local function refresh_favourites_menu()
menu.options = res
if sorted then
- set_menu_sort(menu, true)
+ menu.set_sort(true, sort_options)
end
if opt then
cursor_to_object(opt.id)
@@ -850,56 +806,17 @@ local function search_menu_options(options)
return ret
end
-function update_search_matches()
- local menu = state:menu()
-
- if #menu.search_text == 0 then
- menu.options = menu.search_options
- update_osd()
- return
- end
-
- -- no utf8 :(
- local case_sensitive = not not menu.search_text:find('%u')
-
- local options = {}
- for _, v in ipairs(menu.search_options) do
- local matches = {}
-
- local name = v.name
- if not case_sensitive then
- name = name:lower()
- end
-
- local i, j = 0, 0
- while true do
- i, j = name:find(menu.search_text, j + 1, true)
- if not i then
- break
- end
- matches[#matches+1] = {start = i, stop = j}
- end
-
- if #matches > 0 then
- local t = util.copy_table(v)
- t.matches = matches
- options[#options+1] = t
- end
- end
-
- menu.options = options
-end
-
local function search_input_char(event)
if event.event ~= 'down' and event.event ~= 'repeat' then
return
end
local menu = state:menu()
- menu.search_text = menu.search_text:sub(1, menu.search_cursor - 1) ..
- event.key_text .. menu.search_text:sub(menu.search_cursor)
- menu.search_cursor = menu.search_cursor + #event.key_text
- update_search_matches()
+ menu:set_search_text(
+ menu.search_text:sub(1, menu.search_cursor - 1) ..
+ event.key_text ..
+ menu.search_text:sub(menu.search_cursor))
+ menu:set_search_cursor(menu.search_cursor + #event.key_text)
update_osd()
end
@@ -910,10 +827,10 @@ local function search_input_bs()
end
local pos = util.utf8_seek(menu.search_text, menu.search_cursor, -1)
- menu.search_text = menu.search_text:sub(1, pos - 1) ..
- menu.search_text:sub(menu.search_cursor)
- menu.search_cursor = pos
- update_search_matches()
+ menu:set_search_text(
+ menu.search_text:sub(1, pos - 1) ..
+ menu.search_text:sub(menu.search_cursor))
+ menu:set_search_cursor(pos)
update_osd()
end
@@ -923,22 +840,17 @@ local function search_input_del()
return
end
- menu.search_text = menu.search_text:sub(1, menu.search_cursor - 1) ..
+ menu:set_search_text(
+ menu.search_text:sub(1, menu.search_cursor - 1) ..
menu.search_text:sub(util.utf8_seek(
- menu.search_text, menu.search_cursor, 1))
- update_search_matches()
+ menu.search_text, menu.search_cursor, 1)))
update_osd()
end
local function set_search_cursor(pos)
- local menu = state:menu()
- local pos = math.max(1, math.min(#menu.search_text + 1, pos))
- if pos == menu.search_cursor then
- return
+ if state:menu():set_search_cursor(pos) then
+ update_osd()
end
-
- menu.search_cursor = pos
- update_osd()
end
local function search_cursor_left()
@@ -977,19 +889,15 @@ local function start_search()
menu.title = title
menu.search_active = true
- menu.search_cursor = #menu.search_text + 1
- menu.cursor = 1
- menu.view_top = 1
+ menu:set_search_cursor(#menu.search_text + 1)
+ menu:set_cursor(1, osd_menu_lines())
else
- state:push_menu({
+ menu = state:push_menu({
title = title,
type = 'search',
+ options = search_menu_options(menu.options),
search_active = true,
- search_options = search_menu_options(menu.options),
- search_text = '',
- search_cursor = 1,
})
- update_search_matches()
end
update_osd()
@@ -1010,10 +918,9 @@ local function cancel_search()
-- cancelling resumed search restores previous state
if menu.prev_search_text then
- menu.search_text = menu.prev_search_text
+ menu:set_search_text(menu.prev_search_text)
menu.cursor = menu.prev_cursor
menu.view_top = menu.prev_view_top
- update_search_matches()
end_search()
return
end
@@ -1030,7 +937,7 @@ local function toggle_menu_sort()
return
end
- set_menu_sort(menu, not menu.sorted)
+ menu:set_sort(not menu.sorted, sort_options)
update_osd()
end
diff --git a/state.lua b/state.lua
index 472e4d2..2f609c9 100644
--- a/state.lua
+++ b/state.lua
@@ -1,9 +1,14 @@
-- Copyright 2025 David Vazgenovich Shakaryan
+local util = require('util')
+
local state = {}
local mt = {}
mt.__index = mt
+local menu_mt = {}
+menu_mt.__index = menu_mt
+
function state.new()
return setmetatable({
menus = {},
@@ -19,18 +24,28 @@ function mt:menu()
end
function mt:push_menu(t)
- local menu = {
+ local menu = setmetatable({
options = {},
cursor = 1,
view_top = 1,
- }
+ }, menu_mt)
for k, v in pairs(t) do
menu[k] = v
end
+ if menu.type == 'search' then
+ menu.search_options = menu.options
+ menu.search_text = menu.search_text or ''
+ menu.search_cursor = menu.search_cursor or
+ #menu.search_text + 1
+ menu:update_search_matches()
+ end
+
self.depth = self.depth + 1
self.menus[self.depth] = menu
+
+ return menu
end
-- returns index if found
@@ -67,4 +82,99 @@ function mt:insert_favourite_before_next_in_menu(id)
self:add_favourite(id)
end
+function menu_mt:set_cursor(pos, lines, opts)
+ local pos = math.max(1, math.min(pos, #self.options))
+ local top = self.view_top
+ if opts and opts.centre then
+ top = pos - math.floor((lines - 1) / 2)
+ elseif opts and opts.keep_offset then
+ top = top + pos - self.cursor
+ end
+
+ -- move view to keep selected option visible
+ if pos < top then
+ top = pos
+ elseif pos > top + lines - 1 then
+ top = pos - lines + 1
+ end
+
+ top = math.max(1, math.min(top, #self.options - lines + 1))
+
+ self.cursor = pos
+ self.view_top = top
+end
+
+function menu_mt:set_sort(bool, f)
+ if not self.sorted == not bool then
+ return
+ end
+
+ local key = self.type == 'search' and 'search_options' or 'options'
+ if bool then
+ self['orig_' .. key] = self[key]
+ self[key] = util.copy_table(self[key])
+ f(self[key])
+ else
+ self[key] = self['orig_' .. key]
+ self['orig_' .. key] = nil
+ end
+
+ if self.type == 'search' then
+ self:update_search_matches()
+ end
+ self.sorted = bool
+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
+ return false
+ end
+
+ self.search_cursor = pos
+ return true
+end
+
+function menu_mt:set_search_text(str)
+ self.search_text = str
+ self:update_search_matches()
+end
+
+function menu_mt:update_search_matches()
+ if #self.search_text == 0 then
+ self.options = self.search_options
+ return
+ end
+
+ -- no utf8 :(
+ local case_sensitive = not not self.search_text:find('%u')
+
+ local options = {}
+ for _, v in ipairs(self.search_options) do
+ local matches = {}
+
+ local name = v.name
+ if not case_sensitive then
+ name = name:lower()
+ end
+
+ local i, j = 0, 0
+ while true do
+ i, j = name:find(self.search_text, j + 1, true)
+ if not i then
+ break
+ end
+ matches[#matches+1] = {start = i, stop = j}
+ end
+
+ if #matches > 0 then
+ local t = util.copy_table(v)
+ t.matches = matches
+ options[#options+1] = t
+ end
+ end
+
+ self.options = options
+end
+
return state