summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.lua422
1 files changed, 202 insertions, 220 deletions
diff --git a/main.lua b/main.lua
index 72bf710..552cd14 100644
--- a/main.lua
+++ b/main.lua
@@ -2,11 +2,23 @@
local utils = require('mp.utils')
+-- font size is in units of osd height, which is scaled to 720
+local font_size = 20
+local colours = {
+ default='{\\c}',
+ category='{\\c&H99DDFF&}',
+ selected='{\\c&HFF00&}',
+ title='{\\c&999999&}',
+ search_hl='{\\c&FFDD&}',
+ search_path='{\\c&666666&}',
+ icon_playing='{\\c&HFF6633&}',
+ icon_favourite='{\\c&HFF00FF&}',
+ bg='{\\alpha&H44&\\c&H&}',
+}
+
local script_dir = mp.get_script_directory()
local stream_prefix = mp.get_opt('iptv_menu.stream_prefix')
--- font size is in units of osd height, which is scaled to 720
-local font_size = 20
local osd = mp.create_osd_overlay('ass-events')
local osd_lines = math.floor((720 / font_size) + 0.5) - 1
local osd_padding = math.floor((720 - (osd_lines * font_size)) / 2)
@@ -16,48 +28,59 @@ local osd_cursor_glyph = '{\\p1\\pbo' .. math.floor(font_size / 5) .. '}' ..
' 0 ' .. font_size .. '{\\p0}'
local osd_bg = mp.create_osd_overlay('ass-events')
osd_bg.z = -1
-osd_bg.data = '{\\alpha&H44&\\c&H&\\pos(0,0)}' ..
- '{\\p1}m 0 0 l 9999 0 9999 720 0 720{\\p0}'
+osd_bg.data = '{\\pos(0,0)}' .. colours.bg ..
+ '{\\p1}m 0 0 l 7680 0 7680 720 0 720{\\p0}'
-local categories = {}
-local streams = {}
-local favourites = {}
+local categories
+local streams
+local favourites
local playing_stream_id
local depth = 0
local menus = {}
local key_bindings = {}
-local function load_json_file(path)
- local f = io.open(script_dir .. '/' .. path, 'r')
+local function copy_table(t)
+ local u = {}
+ for k, v in pairs(t) do
+ u[k] = v
+ end
+ return u
+end
+
+local function read_json_file(fn)
+ local f = io.open(script_dir .. '/' .. fn, 'r')
if not f then
return {}
end
- local data = f:read('*all')
+ local json = f:read('*all')
f:close()
- return utils.parse_json(data)
+ return utils.parse_json(json)
end
-local function save_json_file(path, d)
- local data = utils.format_json(d)
- local f = io.open(script_dir .. '/' .. path, 'w')
- f:write(data, '\n')
+local function write_json_file(fn, data)
+ local f = io.open(script_dir .. '/' .. fn, 'w')
+ f:write(utils.format_json(data), '\n')
f:close()
end
local function load_data()
- categories = load_json_file('categories.json')
+ categories = read_json_file('categories.json')
for _, v in ipairs(categories) do
v.type = 'category'
+ v.id = v.category_id
v.name = v.category_name
+ v.parent_id = tostring(v.parent_id)
end
- streams = load_json_file('streams.json')
+ streams = read_json_file('streams.json')
for _, v in ipairs(streams) do
v.type = 'stream'
+ v.id = v.stream_id
+ v.parent_id = v.category_id
end
- favourites = load_json_file('favourites.json')
+ favourites = read_json_file('favourites.json')
-- json loading/dumping breaks when the table is empty, so we need a
-- dummy value to prevent that
if next(favourites) == nil then
@@ -79,40 +102,39 @@ local function update_osd()
if depth > 1 then
for i = 2, depth do
- local col = '999999'
+ local col = colours.title
if menus[i].search_active then
- col = 'FF00'
+ col = colours.selected
end
- out[#out+1] = '{\\c&H' .. col .. '&} » [' ..
- menus[i].title .. ']{\\c}'
+ out[#out+1] = col .. ' » [' .. menus[i].title .. ']'
end
out[#out+1] = ' ' -- space character for correct line height
end
local menu = menus[depth]
- for i = menu.page_pos, math.min(
- menu.page_pos + osd_menu_lines() - 1, #menu.options) do
+ for i = menu.view_top, math.min(
+ menu.view_top + osd_menu_lines() - 1, #menu.options) do
local opt = menu.options[i]
local str = opt.name
- local col = '{\\c}'
+ local col = colours.default
local icons = ''
- if i == menu.cursor_pos and not menu.search_active then
- col = '{\\c&HFF00&}'
+ if i == menu.cursor and not menu.search_active then
+ col = colours.selected
icons = col .. '› ' .. icons
elseif opt.type == 'category' then
- col = '{\\c&H99DDFF&}'
+ col = colours.category
end
- if opt.hl then
+ if opt.matches then
local buf = ''
local n = 0
- for _, hl in ipairs(opt.hl) do
+ for _, match in ipairs(opt.matches) do
buf = buf .. col ..
- str:sub(n + 1, hl.m_start - 1) ..
- '{\\c&HFFDD}' ..
- str:sub(hl.m_start, hl.m_end)
- n = hl.m_end
+ str:sub(n + 1, match.start - 1) ..
+ colours.search_hl ..
+ str:sub(match.start, match.stop)
+ n = match.stop
end
str = buf .. col .. str:sub(n + 1)
else
@@ -122,24 +144,18 @@ local function update_osd()
if opt.type == 'category' then
str = col .. '[' .. str .. ']'
elseif opt.stream_id == playing_stream_id then
- icons = icons .. '{\\c&HFF6633&&}\226\143\186 '
+ icons = icons .. colours.icon_playing .. '\226\143\186 '
end
- local id
- if opt.type == 'category' then
- id = opt.category_id
- else
- id = opt.stream_id
- end
- if favourites[opt.type .. ':' .. id] then
- icons = icons .. '{\\c&HFF00FF&&}★ '
+ if favourites[opt.type .. ':' .. opt.id] then
+ icons = icons .. colours.icon_favourite .. '★ '
end
if opt.path and #opt.path > 0 then
- local path = '{\\c&H666666&&}'
+ local path = colours.search_path
for i = #opt.path, 1, -1 do
local node = opt.path[i]
- path = path .. ' « [' .. node .. ']'
+ path = path .. ' « [' .. node.name .. ']'
end
str = str .. path
end
@@ -154,17 +170,17 @@ local function update_osd()
osd_bg:update()
end
-local function advance_cursor(n, opts)
+local function set_cursor(pos, move_view)
local menu = menus[depth]
local lines = osd_menu_lines()
- local pos = math.max(1, math.min(menu.cursor_pos + n, #menu.options))
- local top = menu.page_pos
- if opts and opts.advance_page then
- top = top + n
+ local pos = math.max(1, math.min(pos, #menu.options))
+ local top = menu.view_top
+ if move_view then
+ top = top + pos - menu.cursor
end
- -- move page to keep selected option visible
+ -- move view to keep selected option visible
if pos < top then
top = pos
elseif pos > top + lines - 1 then
@@ -173,94 +189,80 @@ local function advance_cursor(n, opts)
top = math.max(1, math.min(top, #menu.options - lines + 1))
- menu.cursor_pos = pos
- menu.page_pos = top
+ menu.cursor = pos
+ menu.view_top = top
update_osd()
end
-local function next_option()
- advance_cursor(1)
+local function cursor_up()
+ set_cursor(menus[depth].cursor - 1)
end
-local function prev_option()
- advance_cursor(-1)
+local function cursor_down()
+ set_cursor(menus[depth].cursor + 1)
end
-local function next_page()
- advance_cursor(osd_menu_lines(), {advance_page=true})
+local function cursor_start()
+ set_cursor(0)
end
-local function prev_page()
- advance_cursor(-osd_menu_lines(), {advance_page=true})
+local function cursor_end()
+ set_cursor(#menus[depth].options)
end
-local function first_option()
- advance_cursor(-math.huge)
+local function cursor_page_up()
+ set_cursor(menus[depth].cursor - osd_menu_lines(), true)
end
-local function last_option()
- advance_cursor(math.huge)
+local function cursor_page_down()
+ set_cursor(menus[depth].cursor + osd_menu_lines(), true)
+end
+
+local function prev_menu()
+ if depth > 1 then
+ depth = depth - 1
+ update_osd()
+ end
end
local function push_menu(t)
local menu = {
options={},
- title=title,
- cursor_pos=1,
- page_pos=1,
+ cursor=1,
+ view_top=1,
}
- if t then
- for k, v in pairs(t) do
- menu[k] = v
- end
+ for k, v in pairs(t) do
+ menu[k] = v
end
depth = depth + 1
menus[depth] = menu
end
-local function favourites_menu_options()
- local options = {}
- for _, v in ipairs(categories) do
- if favourites['category:' .. v.category_id] then
- options[#options+1] = v
- end
- end
- for _, v in ipairs(streams) do
- if favourites['stream:' .. v.stream_id] then
- options[#options+1] = v
- end
- end
- return options
-end
-
local function category_menu_options(category_id)
local options = {}
- for _, v in ipairs(categories) do
- if tostring(v.parent_id) == category_id then
- options[#options+1] = v
- end
- end
- for _, v in ipairs(streams) do
- if v.category_id == category_id then
- options[#options+1] = v
+ for _, arr in ipairs({categories, streams}) do
+ for _, v in ipairs(arr) do
+ local test
+ if category_id == 'favourites' then
+ test = favourites[v.type .. ':' .. v.id]
+ else
+ test = v.parent_id == category_id
+ end
+
+ if test then
+ options[#options+1] = v
+ end
end
end
return options
end
-local function add_category_menu(category_id, category_name)
- local options
- if category_id == 'favourites' then
- options = favourites_menu_options()
- else
- options = category_menu_options(category_id)
- end
-
+local function push_category_menu(category_id, title)
push_menu({
- options=options,
- title=category_name,
+ options=category_menu_options(category_id),
+ title=title,
})
update_osd()
end
@@ -274,10 +276,10 @@ end
local function select_option()
local menu = menus[depth]
- local opt = menu.options[menu.cursor_pos]
+ local opt = menu.options[menu.cursor]
if opt.type == 'category' then
- add_category_menu(opt.category_id, opt.name)
+ push_category_menu(opt.id, opt.name)
else
play_stream(opt.stream_id)
end
@@ -285,65 +287,31 @@ end
local function favourite_option()
local menu = menus[depth]
- local opt = menu.options[menu.cursor_pos]
+ local opt = menu.options[menu.cursor]
- local id
- if opt.type == 'category' then
- id = opt.category_id
- else
- id = opt.stream_id
- end
- local key = opt.type .. ':' .. id
+ local key = opt.type .. ':' .. opt.id
if favourites[key] then
favourites[key] = nil
else
favourites[key] = true
end
- save_json_file('favourites.json', favourites)
+ write_json_file('favourites.json', favourites)
update_osd()
end
-local function prev_menu()
- if depth > 1 then
- depth = depth - 1
- update_osd()
- end
-end
-
-local function bind_key(key, name, func, opts)
- key_bindings[key] = name
- mp.add_forced_key_binding(key, name, func, opts)
-end
-
-local function unbind_keys()
- for _, name in pairs(key_bindings) do
- mp.remove_key_binding(name)
- end
- key_bindings = {}
-end
-
local function split_search_text()
local menu = menus[depth]
-
- return menu.search_text:sub(1, menu.search_text_cursor_pos - 1),
- menu.search_text:sub(menu.search_text_cursor_pos)
+ return menu.search_text:sub(1, menu.search_cursor - 1),
+ menu.search_text:sub(menu.search_cursor)
end
-local function search_update_osd()
+local function update_search_osd()
local bc, ac = split_search_text()
menus[depth].title = 'Searching: ' .. bc .. osd_cursor_glyph .. ac
update_osd()
end
-local function copy_table(t)
- local u = {}
- for k, v in pairs(t) do
- u[k] = v
- end
- return u
-end
-
local function search_menu_options_build(options, t, path)
local menu = menus[depth]
local path = path or {}
@@ -357,11 +325,11 @@ local function search_menu_options_build(options, t, path)
v.path = path
t[v.type][#t[v.type]+1] = v
- if v.type == 'category' then
+ if v.type == 'category' and v.id ~= 'favourites' then
local path = copy_table(path)
- path[#path+1] = v.name
+ path[#path+1] = v
search_menu_options_build(
- category_menu_options(v.category_id), t, path)
+ category_menu_options(v.id), t, path)
end
end
end
@@ -370,6 +338,7 @@ local function search_menu_options(options)
local t = {}
search_menu_options_build(options, t)
+ -- display categories before streams
local ret = t.category or {}
if t.stream then
for _, v in ipairs(t.stream) do
@@ -379,18 +348,18 @@ local function search_menu_options(options)
return ret
end
-local function search_update()
+local function update_search_matches()
local menu = menus[depth]
if #menu.search_text == 0 then
menu.options = menu.search_options
- search_update_osd()
+ update_search_osd()
return
end
- local matches = {}
+ local options = {}
for _, v in ipairs(menu.search_options) do
- local hl = {}
+ local matches = {}
local i, j = 0, 0
while true do
@@ -398,163 +367,176 @@ local function search_update()
if not i then
break
end
- hl[#hl+1] = {m_start=i, m_end=j}
+ matches[#matches+1] = {start=i, stop=j}
end
- if #hl > 0 then
+ if #matches > 0 then
local t = copy_table(v)
- t.hl = hl
- matches[#matches+1] = t
+ t.matches = matches
+ options[#options+1] = t
end
end
- menu.options = matches
- search_update_osd()
+ menu.options = options
+ update_search_osd()
end
-local function search_input(event)
- local menu = menus[depth]
-
+local function search_input_text(event)
if event.event ~= 'down' and event.event ~= 'repeat' then
return
end
+ local menu = menus[depth]
local bc, ac = split_search_text()
menu.search_text = bc .. event.key_text .. ac
- menu.search_text_cursor_pos = menu.search_text_cursor_pos + 1
- search_update()
+ menu.search_cursor = menu.search_cursor + 1
+ update_search_matches()
end
-local function search_backspace()
+local function search_input_bs()
local menu = menus[depth]
-
- if menu.search_text_cursor_pos == 1 then
+ if menu.search_cursor == 1 then
return
end
local bc, ac = split_search_text()
menu.search_text = bc:sub(1, -2) .. ac
- menu.search_text_cursor_pos = menu.search_text_cursor_pos - 1
- search_update()
+ menu.search_cursor = menu.search_cursor - 1
+ update_search_matches()
end
-local function search_delete()
+local function search_input_del()
local menu = menus[depth]
-
- if menu.search_text_cursor_pos == #menu.search_text + 1 then
+ if menu.search_cursor == #menu.search_text + 1 then
return
end
local bc, ac = split_search_text()
menu.search_text = bc .. ac:sub(2)
- search_update()
+ update_search_matches()
end
-local function search_cursor_set_pos(pos)
+local function set_search_cursor(pos)
local menu = menus[depth]
-
local pos = math.max(1, math.min(#menu.search_text + 1, pos))
- if pos == menu.search_text_cursor_pos then
+ if pos == menu.search_cursor then
return
end
- menu.search_text_cursor_pos = pos
- search_update_osd()
+ menu.search_cursor = pos
+ update_search_osd()
end
local function search_cursor_left()
- search_cursor_set_pos(menus[depth].search_text_cursor_pos - 1)
+ set_search_cursor(menus[depth].search_cursor - 1)
end
local function search_cursor_right()
- search_cursor_set_pos(menus[depth].search_text_cursor_pos + 1)
+ set_search_cursor(menus[depth].search_cursor + 1)
end
local function search_cursor_start()
- search_cursor_set_pos(1)
+ set_search_cursor(1)
end
local function search_cursor_end()
- search_cursor_set_pos(#menus[depth].search_text + 1)
+ set_search_cursor(#menus[depth].search_text + 1)
end
local bind_search_keys
local bind_menu_keys
-local function search_start()
+local function start_search()
local menu = menus[depth]
if menu.type == 'search' then
+ -- resume search
menu.search_active = true
- menu.search_text_cursor_pos = #menu.search_text + 1
- menu.cursor_pos = 1
- menu.page_pos = 1
- search_update_osd()
+ menu.search_cursor = #menu.search_text + 1
+ menu.cursor = 1
+ menu.view_top = 1
+ update_search_osd()
else
push_menu({
type='search',
search_active=true,
search_options=search_menu_options(menu.options),
search_text='',
- search_text_cursor_pos=1,
+ search_cursor=1,
})
- search_update()
+ update_search_matches()
end
bind_search_keys()
end
-local function search_finish()
+local function end_search()
local menu = menus[depth]
-
menu.search_active = false
menu.title = 'Search results: ' .. menu.search_text
update_osd()
bind_menu_keys()
end
-local function search_cancel()
+local function cancel_search()
menus[depth].search_active = false
depth = depth - 1
update_osd()
bind_menu_keys()
end
+local function bind_key(key, func, opts)
+ -- unique name is needed for removal
+ local i = #key_bindings+1
+ local name = 'key' .. i
+ key_bindings[i] = name
+ mp.add_forced_key_binding(key, name, func, opts)
+end
+
+local function unbind_keys()
+ for _, key in ipairs(key_bindings) do
+ mp.remove_key_binding(key)
+ end
+ key_bindings = {}
+end
+
function bind_search_keys()
unbind_keys()
- local r = {repeatable=true}
- bind_key('ANY_UNICODE', 'search-input', search_input, {complex=true})
- bind_key('ENTER', 'finish-search', search_finish)
- bind_key('ESC', 'cancel-search', search_cancel)
- bind_key('BS', 'search-backspace', search_backspace, r)
- bind_key('DEL', 'search-delete', search_delete, r)
- bind_key('LEFT', 'search-cursor-left', search_cursor_left, r)
- bind_key('RIGHT', 'search-cursor-right', search_cursor_right, r)
- bind_key('Ctrl+a', 'search-cursor-start', search_cursor_start)
- bind_key('Ctrl+e', 'search-cursor-end', search_cursor_end)
- bind_key('Ctrl+c', 'cancel-search-ctrl-c', search_cancel)
+ bind_key('ANY_UNICODE', search_input_text, {complex=true})
+ bind_key('BS', search_input_bs, {repeatable=true})
+ bind_key('DEL', search_input_del, {repeatable=true})
+
+ bind_key('ENTER', end_search)
+ bind_key('ESC', cancel_search)
+ bind_key('Ctrl+c', cancel_search)
+
+ bind_key('LEFT', search_cursor_left, {repeatable=true})
+ bind_key('RIGHT', search_cursor_right, {repeatable=true})
+ bind_key('Ctrl+a', search_cursor_start)
+ bind_key('Ctrl+e', search_cursor_end)
end
function bind_menu_keys()
unbind_keys()
- local r = {repeatable=true}
- bind_key('DOWN', 'next-option', next_option, r)
- bind_key('UP', 'prev-option', prev_option, r)
- bind_key('PGDWN', 'next-page', next_page, r)
- bind_key('PGUP', 'prev-page', prev_page, r)
- bind_key('HOME', 'first-option', first_option)
- bind_key('END', 'last-option', last_option)
- bind_key('ENTER', 'select-option', select_option)
- bind_key('Ctrl+f', 'favourite-option', favourite_option)
- bind_key('BS', 'prev-menu', prev_menu)
- bind_key('/', 'start-search', search_start)
+ bind_key('BS', prev_menu)
+ bind_key('/', start_search)
+
+ bind_key('ENTER', select_option)
+ bind_key('Ctrl+f', favourite_option)
+
+ bind_key('UP', cursor_up, {repeatable=true})
+ bind_key('DOWN', cursor_down, {repeatable=true})
+ bind_key('HOME', cursor_start)
+ bind_key('END', cursor_end)
+ bind_key('PGUP', cursor_page_up, {repeatable=true})
+ bind_key('PGDWN', cursor_page_down, {repeatable=true})
end
local function toggle_menu()
osd.hidden = not osd.hidden
- osd_bg.hidden = osd.hidden
osd:update()
+ osd_bg.hidden = osd.hidden
osd_bg:update()
if osd.hidden then
@@ -580,9 +562,9 @@ mp.add_forced_key_binding('TAB', 'toggle-menu', toggle_menu)
bind_menu_keys()
load_data()
table.insert(categories, 1, {
- name='Favourites',
+ id='favourites',
type='category',
- category_id='favourites',
- parent_id=0,
+ name='Favourites',
+ parent_id='0',
})
-add_category_menu('0')
+push_category_menu('0')