diff options
Diffstat (limited to 'main.lua')
-rw-r--r-- | main.lua | 422 |
1 files changed, 202 insertions, 220 deletions
@@ -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') |