diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf index bb741e8..49294a7 100755 --- a/.config/mpv/input.conf +++ b/.config/mpv/input.conf @@ -1 +1,2 @@ -#d script-message cycle-profiles "bwdifdeint;deinterlace-no" +#d script-message cycle-profiles "bwdifdeint;deinterlace-no" #only needed if using hwdec that has its own deinterlacer and is not bwdif +D vf toggle "pullup,dejudder" diff --git a/.config/mpv/scripts/dvd-browser.lua b/.config/mpv/scripts/dvd-browser.lua new file mode 100644 index 0000000..cf0974b --- /dev/null +++ b/.config/mpv/scripts/dvd-browser.lua @@ -0,0 +1,421 @@ +--[[ + mpv-dvd-browser + + This script uses the `lsdvd` commandline utility to allow users to view and select titles + for DVDs from directly within mpv. The browser is interractive and allows for both playing + the selected title, or appending it to the playlist. + + For full documentation see: https://github.com/CogentRedTester/mpv-dvd-browser +]]-- + +local mp = require 'mp' +local msg = require 'mp.msg' +local opt = require 'mp.options' +local utils = require 'mp.utils' + +local o = { + lsdvd = 'lsdvd', + + --path to the dvd device to send to lsdvd, leaving this blank will set the script to + --use the --dvd-device option. + --It is recommended that this be left blank unless using wsl + dvd_device = "", + + --number of titles to display on the screen at once + num_entries = 20, + + --by default the player enters an infinite loop, usually of the DVD menu screen, after moving + --past the last second of the file. If this option is enabled, then the script will + --automatically configure mpv to end playback before entering the loop + escape_loop = true, + + --changes default mpv behaviour and loads the first title instead of the longest when + --the title isn't specified + start_from_first_title = true, + + --------------------- + --playlist options: + --------------------- + + --adds the previous and subsequent titles to the playlist when playing a dvd + --only does this when there is only one item in the playlist + create_playlist = true, + + --when dvd:// (no specified title) is loaded the script will always insert all of the + --titles into the playlist, regardless of the playlist length + --similar to loading a directory or playlist file + treat_root_as_playlist = true, + + ------------------------------------------ + --wsl options for limitted windows support + ------------------------------------------ + wsl = false, + wsl_password = "", + + -------------- + --ass options + --------------- + ass_header = "{\\q2\\fs35\\c&00ccff&}", + ass_body = "{\\q2\\fs25\\c&Hffffff&}", + ass_selected = "{\\c&Hfce788&}", + ass_playing = "{\\c&H33ff66&}", + ass_footerheader = "{\\c&00ccff&\\fs16}", + ass_cursor = "{\\c&00ccff&}", + ass_length = "{\\fs20\\c&aaaaaa&}" +} + +opt.read_options(o, 'dvd_browser') + +--[[ + lsdvd returns a JSON object with a number of details about the dvd + some notable values are: + title = title of the dvd + longest_track = longest track on the dvd + device = path to the dvd mount point + track = array of 'titles'/tracks on the disc + length = length of each title + ix = numerical id of the title starting from 1 + chapter = array of chapters in the title + num_chapters = length of chapters array (added by me) +]]-- + +--if the script name includes file_browser, then the script is being loaded +--as an addon, and hence we can skip loading several things +local STANDALONE = not mp.get_script_name():find("file_browser") + +local list = {} +if STANDALONE then + package.path = mp.command_native( {"expand-path", "~~/script-modules/?.lua;" } ) .. package.path + list = require "scroll-list" + + list.header_style = o.ass_header + list.list_style = o.ass_body + list.wrapper_style = o.ass_footerheader + list.empty_text = "insert DVD" +end + +local dvd = {} +local state = { + playing_disc = false, + selected = 1, + flag_update = false +} + +--automatically match to the current dvd device +if (o.dvd_device == "") then + mp.observe_property('dvd-device', 'string', function(_, device) + if device == "" then device = "/dev/dvd" end + o.dvd_device = device + + --we set this to false to force a dvd rescan + state.playing_disc = false + end) +end + +local function get_header_str() + local title + if dvd == nil then title = "" + else title = dvd.title end + return '📀 dvd://'..title +end + +local function get_line_str(v) + return "Title "..(v.ix-1)..o.ass_length.."   ["..v.length.."]   "..v.num_chapters.." chapters" +end + +function list:format_header() + self.ass.data = o.ass_header..get_header_str().."\\N ---------------------------------------------------- \\N" +end + +--simple function to append to the ass string +function list:format_line(i, v) + self:append(o.ass_body) + + --the below text contains unicode whitespace characters + if i == list.selected then self:append(o.ass_cursor..[[➤\h]]..o.ass_selected) + else self:append([[\h\h\h\h]]) end + + --prints the currently-playing icon and style + if mp.get_property('filename', "0") == tostring(i-1) then + self:append(o.ass_playing) + end + + self:append(get_line_str(v)) + self:newline() +end + +--sends a call to lsdvd to read the contents of the disc +local function read_disc() + msg.verbose('reading contents of ' .. o.dvd_device) + + local args + if o.wsl then + msg.verbose('wsl compatibility mode enabled') + + --if wsl password is not set then we'll assume the user has mounted manually + if o.wsl_password ~= "" then + local dvd_device = mp.get_property('dvd-device', ''):gsub([[\]], [[/]]) + msg.verbose('mounting '..dvd_device..' at '..o.dvd_device) + + mp.command_native({ + name = 'subprocess', + playback_only = false, + args = {'wsl', 'echo', o.wsl_password, '|', 'sudo', '-S', 'mount', '-t', 'drvfs', dvd_device, o.dvd_device} + }) + end + + --setting wsl arguments + args = {'wsl', o.lsdvd, o.dvd_device, '-Oy', '-c'} + else + args = {o.lsdvd, o.dvd_device, '-Oy', '-c'} + end + + local cmd = mp.command_native({ + name = 'subprocess', + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args + }) + + --making the python string JSON compatible + local result = cmd.stdout:gsub("'", '"') + result = result:gsub('lsdvd = ', '') + dvd = utils.parse_json(result) + + if (not dvd) then + msg.error(cmd.stderr) + state.playing_disc = false + return + end + msg.trace(utils.to_string(dvd)) + + --creating a fallback for the title + -- if dvd.title == "unknown" then dvd.title = "dvd://" end + + --making modifications to all the entries + for i = 1, #dvd.track do + local v = dvd.track[i] + + --saving the chapter count + v.num_chapters = #v.chapter + + --modifying the length + local l = v.length + local lstr = tostring(l) + + --adding the microseconds as is + local index = tostring(l):find([[.[^.]*$]]) + local str + if index == 1 then str = "00" + else + str = tostring(lstr:sub(index+1)) + str = string.format('%02d', str) + end + l = math.floor(l) + + local seconds = l%60 + str = string.format('%02d', seconds) .. '.' .. str + l = (l - seconds)/60 + + local mins = l%60 + str = string.format('%02d', mins) .. ':' .. str + l = (l-mins)/60 + + local hours = l%24 + str = string.format('%02d', hours) .. ':' .. str + + msg.debug('changing length string for title '..(i-1)..' to '..str) + v.length = str + end + + state.playing_disc = true + list.list = dvd.track +end + +--this function updates dvd information and updates the browser +local function update() + read_disc() + list:update() +end + +--appends the specified playlist item along with the desired options +local function load_dvd_title(title, flag) + local i = title.ix-1 + mp.commandv("loadfile", "dvd://"..i, flag) +end + +--handles actions when dvd:// paths are played directly +--updates dvd information and inserts disc titles into the playlist +local function load_disc() + local path = mp.get_property('stream-open-filename', '') + + if path:find('dvd://') ~= 1 then + state.playing_disc = false + return + end + msg.verbose('playing dvd') + + --if we have not stopped playing a disc then there's no need to parse the disc again + if not state.playing_disc then read_disc() end + + --if we still can't detect a disc then return + if (not state.playing_disc) then return end + + --if we successfully loaded info about the disc it's time to do some other stuff: + --this code block finds the default title of the disc + local curr_title + + --if the user specified a title number we use that + if path ~= "dvd://" then + --treating whatever comes after "dvd://" as the title number + curr_title = tonumber(path:sub(7)) + + --if dvd:// was sent and this option is set we set the default ourselves + elseif o.start_from_first_title then + mp.set_property('stream-open-filename', "dvd://0") + curr_title = 0 + + --otherwise if just dvd:// was sent we need to find the longest title + else + curr_title = dvd.longest_track + end + + mp.set_property('file-local-options/title', dvd.title.." - Title "..curr_title) + + --if o.create_playlist is false then the function can end here + if not o.create_playlist then return end + + --offsetting curr_title by one to account for lua arrays being 1-based + curr_title = curr_title+1 + local length = mp.get_property_number('playlist-count', 1) + + --load files in the playlist under the specified conditions + if (path == "dvd://" and o.treat_root_as_playlist) or length == 1 then + local pos = mp.get_property_number('playlist-pos', 1) + + --add all of the files to the playlist + for i = 1, #dvd.track do + if i ~= curr_title then + load_dvd_title(dvd.track[i], "append") + length = length + 1 + + --we need slightly different behaviour when prepending vs appending a playlist entry + if (i < curr_title) then + mp.commandv("playlist-move", length-1, pos) + pos = pos+1 + elseif (i > curr_title) then + mp.commandv("playlist-move", length-1, pos+(i-curr_title)) + end + end + end + + --if the path is dvd, then we actually need to fully replace this entry in the playlist, + --otherwise the whole disc will be added to the playlist again if moving back to this entry + if (path == "dvd://") then + msg.verbose('replacing dvd:// with playlist') + + load_dvd_title(dvd.track[curr_title], "append") + length = length+1 + mp.commandv('playlist-move', length-1, pos+1) + mp.commandv('playlist-remove', 'current') + end + end +end + +--opens the currently selected file +local function open_file(flag) + load_dvd_title(dvd.track[list.selected], flag) + + if flag == 'replace' then + list:close() + end +end + +--opens the browser and declares dynamic keybinds +function list:open() + self.hidden = false + if not state.playing_disc then + update() + else + self:open_list() + end + self:add_keybinds() +end + +list.keybinds = { + {"ESC", "exit", function() list:close() end, {}}, + {"ENTER", "open", function() open_file('replace') end, {}}, + {"Shift+ENTER", "append_playlist", function() open_file('append') end, {}}, + {'DOWN', 'scroll_down', function() list:scroll_down() end, {repeatable = true}}, + {'UP', 'scroll_up', function() list:scroll_up() end, {repeatable = true}}, + {'Ctrl+r', 'reload', function() read_disc() ; list:update() end, {}} +} + +--modifies track length to escape infinite loop +if o.escape_loop then + mp.add_hook('on_preloaded', 50, function() + if mp.get_property("path", ""):find("dvd://") ~= 1 then return end + if mp.get_property('end', 'none') ~= 'none' then return end + + local chapters = mp.get_property_native('chapter-list') + if not chapters then return end + + local num_chapters = #chapters + + -- occurs if there are no chapters + if not chapters[num_chapters] then return end + if (mp.get_property_number('duration', 0) - (chapters[num_chapters].time or 0)) > 1 then return end + + msg.verbose('modifying end of the title to escape infinite loop') + mp.set_property('file-local-options/end', "#"..num_chapters) + end) +end + +--if we're playing a disc then read it and modify playlist appropriately +mp.add_hook('on_load', 50, load_disc) + +--if these events are disabled then the list gui functions are never called +if STANDALONE then + mp.observe_property('path', 'string', function(_,path) + if state.playing_disc then list:update() end + end) + + mp.register_script_message('browse-dvd', function() list:open() end) + + mp.add_key_binding('MENU', 'dvd-browser', function() list:toggle() end) +end + +--module functions when loading as file_browser addon +local dvd_module = { + priority = 50 +} + +function dvd_module:can_parse(directory) + return directory:sub(1,6) == "dvd://" or directory == self.get_dvd_device() +end + +function dvd_module:parse() + read_disc() + local list = {} + + if dvd then + for i = 1, #dvd.track do + list[i] = { + ass = get_line_str(dvd.track[i]), + name = tostring(dvd.track[i].ix-1), + path = "dvd://"..(dvd.track[i].ix-1), + type = "file" + } + end + end + + return list, { + empty_text = "insert DVD", + directory_label = get_header_str(), + filtered = true, + sorted = true + } +end + +return dvd_module diff --git a/.config/mpv/scripts/dvd_browser.conf b/.config/mpv/scripts/dvd_browser.conf new file mode 100644 index 0000000..785b8ce --- /dev/null +++ b/.config/mpv/scripts/dvd_browser.conf @@ -0,0 +1,65 @@ +################################################################# +######### Default configuration file for mpv-dvd-browser ######## +####### https://github.com/CogentRedTester/mpv-dvd-browser ###### +################################################################# + +#path to the lsdvd executable +#searches the system path by default +lsdvd=lsdvd + +#path to the dvd device to send to lsdvd, leaving this blank will set the script to +#use the --dvd-device option. +#It is recommended that this be left blank unless using wsl +dvd_device= + +#number of titles to display on the screen at once +num_entries=20 + +#by default the player enters an infinite loop, usually of the DVD menu screen, after moving +#past the last second of the file. If this option is enabled, then the script will +#automatically configure mpv to end playback before entering the loop +escape_loop=yes + +#changes default mpv behaviour and loads the first title instead of the longest when +#the title isn't specified +start_from_first_title=yes + +######################## +### playlist options ### +######################## + +#adds the previous and subsequent titles to the playlist when playing a dvd +#only does this when there is only one item in the playlist +create_playlist=yes + +#when dvd:// (no specified title) is loaded the script will always insert all of the +#titles into the playlist, regardless of the playlist length +#similar to loading a directory or playlist file +treat_root_as_playlist=yes + +################################################## +#### wsl options for limitted windows support #### +################################################## + +#enable wsl compatibility mode +wsl=no + +#your WSL user password for running the `sudo mount` command +#leaving this blank will disable the auto-mounting command +wsl_password= + + +########################################################################################### +# ass tags to change the look of the menu +# For information see: http://docs.aegisub.org/3.2/ASS_Tags/ +# +# It's recommended not to put these in your config file unless you know what you're doing, +# otherwise any improvements I make to the default theme will be overwritten +########################################################################################### +ass_header={\q2\fs35\c&00ccff&} +ass_body={\q2\fs25\c&Hffffff&} +ass_selected={\c&Hfce788&} +ass_playing={\c&H33ff66&} +ass_footerheader={\c&00ccff&\fs16} +ass_cursor={\c&00ccff&} +ass_length={\fs20\c&aaaaaa&} diff --git a/.config/mpv/scripts/manga-reader.lua b/.config/mpv/scripts/manga-reader.lua index f5725de..0420fbb 100644 --- a/.config/mpv/scripts/manga-reader.lua +++ b/.config/mpv/scripts/manga-reader.lua @@ -19,6 +19,7 @@ local ext = { local double_page_check = false local first_start = true local filedims = {} +local format = {} local initiated = false local input = "" local jump = false @@ -34,11 +35,11 @@ local opts = { double = false, manga = true, pan_size = 0.05, - similar_height_threshold = 50, + similar_height_threshold = 200, skip_size = 10, trigger_zone = 0.05, - zoom_multiplier = 1, } +local lavfi_format = {} local lavfi_scale = {} local similar_height = {} local valid_width = {} @@ -50,25 +51,6 @@ function add_tracks(start, finish) end end -function calculate_zoom_level(dims, pages) - local display_width = mp.get_property_number("display-width") - local display_height = mp.get_property_number("display-height") - local display_dpi = mp.get_property_number("display-hidpi-scale") - - display_width = display_width / display_dpi - display_height = display_height / display_dpi - - dims[0] = tonumber(dims[0]) - dims[1] = tonumber(dims[1]) * opts.continuous_size - - local scaled_width = display_height/dims[1] * dims[0] - if display_width >= opts.continuous_size*scaled_width then - return pages - else - return display_width / scaled_width - end -end - function check_aspect_ratio(index) local a = filedims[index] local b = filedims[index+1] @@ -106,6 +88,14 @@ function check_double_page_dims(index) double_page_check = false end +function check_gray_format(name) + if name and string.sub(name, 1, 4) == "gray" then + return true + else + return false + end +end + function check_images() local audio = mp.get_property("audio-params") local image = mp.get_property_bool("current-tracks/video/image") @@ -128,6 +118,9 @@ function set_custom_title(last_index) end function create_modes() + if first_start and not opts.auto_start then + return + end local index = mp.get_property_number("playlist-pos") local len = mp.get_property_number("playlist-count") local pages @@ -143,7 +136,7 @@ function create_modes() finish = len - 1 end add_tracks(index, finish) - store_file_dims(index, finish) + store_file_props(index, finish) if opts.double then check_double_page_dims(index) set_lavfi_complex_double() @@ -160,7 +153,7 @@ function create_modes() end end -function store_file_dims(start, finish) +function store_file_props(start, finish) local len = mp.get_property_number("playlist-count") local needs_dims = false for i=start, finish do @@ -191,12 +184,21 @@ function store_file_dims(start, finish) dims[0] = width dims[1] = height filedims[i+start] = dims + format[i+start] = mp.get_property("track-list/"..tostring(i).."/format-name") + -- special case any yuvj formats to avoid ffmpeg deprecation warning spam + if format[i+start] and string.sub(format[i+start], 1, 4) == "yuvj" then + format[i+start] = string.gsub(format[i+start], "j", "") + end end for i=start, finish - 1 do valid_width[i] = check_aspect_ratio(i) if filedims[i][1] ~= filedims[i+1][1] then lavfi_scale[i] = true end + if format[i] ~= format[i+1] and check_gray_format(format[i]) or check_gray_format(format[i+1]) then + -- if one page is gray, we need to forcibly convert it + lavfi_format[i] = check_gray_format(format[i]) and format[i+1] or format[i] + end if math.abs(filedims[i][1] - filedims[i+1][1]) < opts.similar_height_threshold then similar_height[i] = true else @@ -232,6 +234,27 @@ function set_lavfi_complex_continuous(arg, finish) local index = mp.get_property_number("playlist-pos") local pages = finish - index local max_width = find_max_width(pages) + local has_gray = false + local has_color = false + local color_format = "" + for i=0, pages do + if check_gray_format(format[index+i]) then + has_gray = true + else + has_color = true + color_format = format[index+i] + end + end + -- if at least one page is color, any gray pages must be converted + if has_gray and has_color then + for i=0, pages do + if check_gray_format(format[index+i]) then + local split_format = string.gsub(split[i], "]", "_format]") + vstack = vstack..split[i].." format="..color_format.. " "..split_format.."; " + split[i] = split_format + end + end + end for i=0, pages do if filedims[index+i][0] ~= max_width then local split_pad = string.gsub(split[i], "]", "_pad]") @@ -245,8 +268,6 @@ function set_lavfi_complex_continuous(arg, finish) vstack = vstack.."vstack=inputs="..tostring(pages + 1).." [vo]" mp.set_property("lavfi-complex", vstack) local index = mp.get_property_number("playlist-pos") - local zoom_level = calculate_zoom_level(filedims[index], pages+1) - mp.set_property_number("video-zoom", opts.zoom_multiplier * log2(zoom_level)) mp.set_property_number("video-pan-y", 0) if upwards then mp.set_property_number("video-align-y", 1) @@ -265,18 +286,26 @@ function set_lavfi_complex_double() end return end - local hstack - local external_vid = "[vid2]" + local hstack = "" + local vid1 = "[vid1]" + local vid2 = "[vid2]" + if lavfi_format[index] then + if check_gray_format(format[index]) then + hstack = vid1.." format="..lavfi_format[index].." [vid1_format]; " + vid1 = "[vid1_format]" + else + hstack = vid2.." format="..lavfi_format[index].." [vid2_format]; " + vid2 = "[vid2_format]" + end + end if lavfi_scale[index] then - external_vid = string.sub(external_vid, 0, 5).."_scale]" + hstack = hstack..vid2.." scale="..filedims[index][0].."x"..filedims[index][1]..":flags=lanczos [vid2_scale]; " + vid2 = "[vid2_scale]" end if opts.manga then - hstack = external_vid.." [vid1] hstack [vo]" + hstack = hstack..vid2.." "..vid1.. " hstack [vo]" else - hstack = "[vid1] "..external_vid.." hstack [vo]" - end - if lavfi_scale[index] then - hstack = "[vid2] scale="..filedims[index][0].."x"..filedims[index][1]..":flags=lanczos [vid2_scale]; "..hstack + hstack = hstack..vid1.." "..vid2.. " hstack [vo]" end mp.set_property("lavfi-complex", hstack) end @@ -631,6 +660,7 @@ function toggle_reader() set_properties() mp.observe_property("playlist-count", number, remove_non_images) mp.osd_message("Manga Reader Started") + mp.add_hook("on_preloaded", 50, create_modes) mp.add_key_binding("c", "toggle-continuous-mode", toggle_continuous_mode) mp.add_key_binding("d", "toggle-double-page", toggle_double_page) mp.add_key_binding("m", "toggle-manga-mode", toggle_manga_mode) @@ -642,7 +672,6 @@ function toggle_reader() restore_properties() mp.unobserve_property(check_y_pos) mp.unobserve_property(remove_non_images) - mp.set_property_number("video-zoom", 0) mp.set_property_number("video-align-y", 0) mp.set_property_number("video-pan-y", 0) mp.set_property("lavfi-complex", "") @@ -723,7 +752,6 @@ function toggle_continuous_mode() opts.continuous = false mp.unobserve_property(check_y_pos) mp.set_property("lavfi-complex", "") - mp.set_property_number("video-zoom", 0) mp.set_property_number("video-align-y", 0) mp.set_property_number("video-pan-y", 0) else @@ -764,7 +792,6 @@ function toggle_manga_mode() mp.commandv("playlist-play-index", index) end -mp.add_hook("on_preloaded", 50, create_modes) mp.register_event("file-loaded", init) mp.add_key_binding("y", "toggle-reader", toggle_reader) require "mp.options".read_options(opts, "manga-reader") diff --git a/.config/mpv/scripts/scroll-list.lua b/.config/mpv/scripts/scroll-list.lua new file mode 100644 index 0000000..5d8f9fa --- /dev/null +++ b/.config/mpv/scripts/scroll-list.lua @@ -0,0 +1,293 @@ +local mp = require 'mp' +local scroll_list = { + global_style = [[]], + header_style = [[{\q2\fs35\c&00ccff&}]], + list_style = [[{\q2\fs25\c&Hffffff&}]], + wrapper_style = [[{\c&00ccff&\fs16}]], + cursor_style = [[{\c&00ccff&}]], + selected_style = [[{\c&Hfce788&}]], + + cursor = [[➤\h]], + indent = [[\h\h\h\h]], + + num_entries = 16, + wrap = false, + empty_text = "no entries" +} + +--formats strings for ass handling +--this function is based on a similar function from https://github.com/mpv-player/mpv/blob/master/player/lua/console.lua#L110 +function scroll_list.ass_escape(str, replace_newline) + if replace_newline == true then replace_newline = "\\\239\187\191n" end + + --escape the invalid single characters + str = str:gsub('[\\{}\n]', { + -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if + -- it isn't followed by a recognised character, so add a zero-width + -- non-breaking space + ['\\'] = '\\\239\187\191', + ['{'] = '\\{', + ['}'] = '\\}', + -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of + -- consecutive newlines + ['\n'] = '\239\187\191\\N', + }) + + -- Turn leading spaces into hard spaces to prevent ASS from stripping them + str = str:gsub('\\N ', '\\N\\h') + str = str:gsub('^ ', '\\h') + + if replace_newline then + str = str:gsub("\\N", replace_newline) + end + return str +end + +--format and return the header string +function scroll_list:format_header_string(str) + return str +end + +--appends the entered text to the overlay +function scroll_list:append(text) + if text == nil then return end + self.ass.data = self.ass.data .. text + end + +--appends a newline character to the osd +function scroll_list:newline() + self.ass.data = self.ass.data .. '\\N' +end + +--re-parses the list into an ass string +--if the list is closed then it flags an update on the next open +function scroll_list:update() + if self.hidden then self.flag_update = true + else self:update_ass() end +end + +--prints the header to the overlay +function scroll_list:format_header() + self:append(self.header_style) + self:append(self:format_header_string(self.header)) + self:newline() +end + +--formats each line of the list and prints it to the overlay +function scroll_list:format_line(index, item) + self:append(self.list_style) + + if index == self.selected then self:append(self.cursor_style..self.cursor..self.selected_style) + else self:append(self.indent) end + + self:append(item.style) + self:append(item.ass) + self:newline() +end + +--refreshes the ass text using the contents of the list +function scroll_list:update_ass() + self.ass.data = self.global_style + self:format_header() + + if #self.list < 1 then + self:append(self.empty_text) + self.ass:update() + return + end + + local start = 1 + local finish = start+self.num_entries-1 + + --handling cursor positioning + local mid = math.ceil(self.num_entries/2)+1 + if self.selected+mid > finish then + local offset = self.selected - finish + mid + + --if we've overshot the end of the list then undo some of the offset + if finish + offset > #self.list then + offset = offset - ((finish+offset) - #self.list) + end + + start = start + offset + finish = finish + offset + end + + --making sure that we don't overstep the boundaries + if start < 1 then start = 1 end + local overflow = finish < #self.list + --this is necessary when the number of items in the dir is less than the max + if not overflow then finish = #self.list end + + --adding a header to show there are items above in the list + if start > 1 then self:append(self.wrapper_style..(start-1)..' item(s) above\\N\\N') end + + for i=start, finish do + self:format_line(i, self.list[i]) + end + + if overflow then self:append('\\N'..self.wrapper_style..#self.list-finish..' item(s) remaining') end + self.ass:update() +end + +--moves the selector down the list +function scroll_list:scroll_down() + if self.selected < #self.list then + self.selected = self.selected + 1 + self:update_ass() + elseif self.wrap then + self.selected = 1 + self:update_ass() + end +end + +--moves the selector up the list +function scroll_list:scroll_up() + if self.selected > 1 then + self.selected = self.selected - 1 + self:update_ass() + elseif self.wrap then + self.selected = #self.list + self:update_ass() + end +end + +--moves the selector to the list next page +function scroll_list:move_pagedown() + if #self.list > self.num_entries then + self.selected = self.selected + self.num_entries + if self.selected > #self.list then self.selected = #self.list end + self:update_ass() + end +end + +--moves the selector to the list previous page +function scroll_list:move_pageup() + if #self.list > self.num_entries then + self.selected = self.selected - self.num_entries + if self.selected < 1 then self.selected = 1 end + self:update_ass() + end +end + +--moves the selector to the list begin +function scroll_list:move_begin() + if #self.list > 1 then + self.selected = 1 + self:update_ass() + end +end + +--moves the selector to the list end +function scroll_list:move_end() + if #self.list > 1 then + self.selected = #self.list + self:update_ass() + end +end + +--adds the forced keybinds +function scroll_list:add_keybinds() + for _,v in ipairs(self.keybinds) do + mp.add_forced_key_binding(v[1], 'dynamic/'..self.ass.id..'/'..v[2], v[3], v[4]) + end +end + +--removes the forced keybinds +function scroll_list:remove_keybinds() + for _,v in ipairs(self.keybinds) do + mp.remove_key_binding('dynamic/'..self.ass.id..'/'..v[2]) + end +end + +--opens the list and sets the hidden flag +function scroll_list:open_list() + self.hidden = false + if not self.flag_update then self.ass:update() + else self.flag_update = false ; self:update_ass() end +end + +--closes the list and sets the hidden flag +function scroll_list:close_list() + self.hidden = true + self.ass:remove() +end + +--modifiable function that opens the list +function scroll_list:open() + if self.hidden then self:add_keybinds() end + self:open_list() +end + +--modifiable function that closes the list +function scroll_list:close() + self:remove_keybinds() + self:close_list() +end + +--toggles the list +function scroll_list:toggle() + if self.hidden then self:open() + else self:close() end +end + +--clears the list in-place +function scroll_list:clear() + local i = 1 + while self.list[i] do + self.list[i] = nil + i = i + 1 + end +end + +--added alias for ipairs(list.list) for lua 5.1 +function scroll_list:ipairs() + return ipairs(self.list) +end + +--append item to the end of the list +function scroll_list:insert(item) + self.list[#self.list + 1] = item +end + +local metatable = { + __index = function(t, key) + if scroll_list[key] ~= nil then return scroll_list[key] + elseif key == "__current" then return t.list[t.selected] + elseif type(key) == "number" then return t.list[key] end + end, + __newindex = function(t, key, value) + if type(key) == "number" then rawset(t.list, key, value) + else rawset(t, key, value) end + end, + __scroll_list = scroll_list, + __len = function(t) return #t.list end, + __ipairs = function(t) return ipairs(t.list) end +} + +--creates a new list object +function scroll_list:new() + local vars + vars = { + ass = mp.create_osd_overlay('ass-events'), + hidden = true, + flag_update = true, + + header = "header \\N ----------------------------------------------", + list = {}, + selected = 1, + + keybinds = { + {'DOWN', 'scroll_down', function() vars:scroll_down() end, {repeatable = true}}, + {'UP', 'scroll_up', function() vars:scroll_up() end, {repeatable = true}}, + {'PGDWN', 'move_pagedown', function() vars:move_pagedown() end, {}}, + {'PGUP', 'move_pageup', function() vars:move_pageup() end, {}}, + {'HOME', 'move_begin', function() vars:move_begin() end, {}}, + {'END', 'move_end', function() vars:move_end() end, {}}, + {'ESC', 'close_browser', function() vars:close() end, {}} + } + } + return setmetatable(vars, metatable) +end + +return scroll_list:new()