init
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
--------------------------------Scroll/Select Implementation--------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
|
||||
local g = require 'modules.globals'
|
||||
local fb_utils = require 'modules.utils'
|
||||
local ass = require 'modules.ass'
|
||||
|
||||
---@class cursor
|
||||
local cursor = {}
|
||||
|
||||
--disables multiselect
|
||||
function cursor.disable_select_mode()
|
||||
g.state.multiselect_start = nil
|
||||
g.state.initial_selection = nil
|
||||
end
|
||||
|
||||
--enables multiselect
|
||||
function cursor.enable_select_mode()
|
||||
g.state.multiselect_start = g.state.selected
|
||||
g.state.initial_selection = fb_utils.copy_table(g.state.selection)
|
||||
end
|
||||
|
||||
--calculates what drag behaviour is required for that specific movement
|
||||
local function drag_select(original_pos, new_pos)
|
||||
if original_pos == new_pos then return end
|
||||
|
||||
local setting = g.state.selection[g.state.multiselect_start or -1]
|
||||
for i = original_pos, new_pos, (new_pos > original_pos and 1 or -1) do
|
||||
--if we're moving the cursor away from the starting point then set the selection
|
||||
--otherwise restore the original selection
|
||||
if i > g.state.multiselect_start then
|
||||
if new_pos > original_pos then
|
||||
g.state.selection[i] = setting
|
||||
elseif i ~= new_pos then
|
||||
g.state.selection[i] = g.state.initial_selection[i]
|
||||
end
|
||||
elseif i < g.state.multiselect_start then
|
||||
if new_pos < original_pos then
|
||||
g.state.selection[i] = setting
|
||||
elseif i ~= new_pos then
|
||||
g.state.selection[i] = g.state.initial_selection[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--moves the selector up and down the list by the entered amount
|
||||
function cursor.scroll(n, wrap)
|
||||
local num_items = #g.state.list
|
||||
if num_items == 0 then return end
|
||||
|
||||
local original_pos = g.state.selected
|
||||
|
||||
if original_pos + n > num_items then
|
||||
g.state.selected = wrap and 1 or num_items
|
||||
elseif original_pos + n < 1 then
|
||||
g.state.selected = wrap and num_items or 1
|
||||
else
|
||||
g.state.selected = original_pos + n
|
||||
end
|
||||
|
||||
if g.state.multiselect_start then drag_select(original_pos, g.state.selected) end
|
||||
ass.update_ass()
|
||||
end
|
||||
|
||||
--selects the first item in the list which is highlighted as playing
|
||||
function cursor.select_playing_item()
|
||||
for i,item in ipairs(g.state.list) do
|
||||
if ass.highlight_entry(item) then
|
||||
g.state.selected = i
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--scans the list for which item to select by default
|
||||
--chooses the folder that the script just moved out of
|
||||
--or, otherwise, the item highlighted as currently playing
|
||||
function cursor.select_prev_directory()
|
||||
-- makes use of the directory stack to more exactly select the prev directory
|
||||
local down_stack = g.directory_stack.stack[g.directory_stack.position + 1]
|
||||
if down_stack then
|
||||
for i, item in ipairs(g.state.list) do
|
||||
if fb_utils.get_new_directory(item, g.state.directory) == down_stack then
|
||||
g.state.selected = i
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if g.state.prev_directory:find(g.state.directory, 1, true) == 1 then
|
||||
for i, item in ipairs(g.state.list) do
|
||||
if
|
||||
g.state.prev_directory:find(fb_utils.get_full_path(item), 1, true) or
|
||||
g.state.prev_directory:find(fb_utils.get_new_directory(item, g.state.directory), 1, true)
|
||||
then
|
||||
g.state.selected = i
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cursor.select_playing_item()
|
||||
end
|
||||
|
||||
--toggles the selection
|
||||
function cursor.toggle_selection()
|
||||
if not g.state.list[g.state.selected] then return end
|
||||
g.state.selection[g.state.selected] = not g.state.selection[g.state.selected] or nil
|
||||
ass.update_ass()
|
||||
end
|
||||
|
||||
--select all items in the list
|
||||
function cursor.select_all()
|
||||
for i,_ in ipairs(g.state.list) do
|
||||
g.state.selection[i] = true
|
||||
end
|
||||
ass.update_ass()
|
||||
end
|
||||
|
||||
--toggles select mode
|
||||
function cursor.toggle_select_mode()
|
||||
if g.state.multiselect_start == nil then
|
||||
cursor.enable_select_mode()
|
||||
cursor.toggle_selection()
|
||||
else
|
||||
cursor.disable_select_mode()
|
||||
ass.update_ass()
|
||||
end
|
||||
end
|
||||
|
||||
return cursor
|
||||
@@ -0,0 +1,209 @@
|
||||
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
local o = require 'modules.options'
|
||||
local g = require 'modules.globals'
|
||||
local ass = require 'modules.ass'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
local fb_utils = require 'modules.utils'
|
||||
local cursor = require 'modules.navigation.cursor'
|
||||
|
||||
---@class directory_movement
|
||||
local directory_movement = {}
|
||||
local NavType = scanning.NavType
|
||||
|
||||
---Appends an item to the directory stack, wiping any
|
||||
---directories further ahead than the current position.
|
||||
---@param dir string
|
||||
local function directory_stack_append(dir)
|
||||
-- don't clear the stack if we're re-entering the same directory
|
||||
if g.directory_stack.stack[g.directory_stack.position + 1] == dir then
|
||||
g.directory_stack.position = g.directory_stack.position + 1
|
||||
return
|
||||
end
|
||||
|
||||
local j = #g.directory_stack.stack
|
||||
while g.directory_stack.position < j do
|
||||
g.directory_stack.stack[j] = nil
|
||||
j = j - 1
|
||||
end
|
||||
table.insert(g.directory_stack.stack, dir)
|
||||
g.directory_stack.position = g.directory_stack.position + 1
|
||||
end
|
||||
|
||||
---@param dir string
|
||||
local function directory_stack_prepend(dir)
|
||||
table.insert(g.directory_stack.stack, 1, dir)
|
||||
g.directory_stack.position = 1
|
||||
end
|
||||
|
||||
---Clears directories from the history
|
||||
---@param from? number All entries >= this index are cleared.
|
||||
---@return string[]
|
||||
function directory_movement.clear_history(from)
|
||||
---@type string[]
|
||||
local cleared = {}
|
||||
|
||||
from = from or 1
|
||||
for i = g.history.size, from, -1 do
|
||||
table.insert(cleared, g.history.list[i])
|
||||
g.history.list[i] = nil
|
||||
g.history.size = g.history.size - 1
|
||||
|
||||
if g.history.position >= i then
|
||||
g.history.position = g.history.position - 1
|
||||
end
|
||||
end
|
||||
|
||||
return cleared
|
||||
end
|
||||
|
||||
---Append a directory to the history
|
||||
---If we have navigated backward in the history,
|
||||
---then clear any history beyond the current point.
|
||||
---@param directory string
|
||||
function directory_movement.append_history(directory)
|
||||
if g.history.list[g.history.position] == directory then
|
||||
msg.debug('reloading same directory - history unchanged:', directory)
|
||||
return
|
||||
end
|
||||
|
||||
msg.debug('appending to history:', directory)
|
||||
if g.history.position < g.history.size then
|
||||
directory_movement.clear_history(g.history.position + 1)
|
||||
end
|
||||
|
||||
table.insert(g.history.list, directory)
|
||||
g.history.size = g.history.size + 1
|
||||
g.history.position = g.history.position + 1
|
||||
|
||||
if g.history.size > o.history_size then
|
||||
table.remove(g.history.list, 1)
|
||||
g.history.size = g.history.size - 1
|
||||
end
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
function directory_movement.set_current_file(filepath)
|
||||
--if we're in idle mode then we want to open the working directory
|
||||
if filepath == nil then
|
||||
g.current_file.directory = fb_utils.fix_path( mp.get_property("working-directory", ""), true)
|
||||
g.current_file.name = nil
|
||||
g.current_file.path = nil
|
||||
g.current_file.original_path = nil
|
||||
return
|
||||
end
|
||||
|
||||
local absolute_path = fb_utils.absolute_path(filepath)
|
||||
local resolved_path = fb_utils.resolve_directory_mapping(absolute_path)
|
||||
|
||||
g.current_file.directory, g.current_file.name = utils.split_path(resolved_path)
|
||||
g.current_file.original_path = absolute_path
|
||||
g.current_file.path = resolved_path
|
||||
|
||||
if o.cursor_follows_playing_item then cursor.select_playing_item() end
|
||||
ass.update_ass()
|
||||
end
|
||||
|
||||
--the base function for moving to a directory
|
||||
---@param directory string
|
||||
---@param nav_type? NavigationType
|
||||
---@param store_history? boolean default `true`
|
||||
---@param parse_properties? ParseProperties
|
||||
---@return thread
|
||||
function directory_movement.goto_directory(directory, nav_type, store_history, parse_properties)
|
||||
local current = g.state.list[g.state.selected]
|
||||
g.state.directory = directory
|
||||
|
||||
if g.state.directory_label then
|
||||
if nav_type == NavType.DOWN then
|
||||
g.state.directory_label = g.state.directory_label..(current.label or current.name)
|
||||
elseif nav_type == NavType.UP then
|
||||
g.state.directory_label = string.match(g.state.directory_label, "^(.-/+)[^/]+/*$")
|
||||
end
|
||||
end
|
||||
|
||||
if o.history_size > 0 and store_history == nil or store_history then
|
||||
directory_movement.append_history(directory)
|
||||
end
|
||||
|
||||
return scanning.rescan(nav_type or NavType.GOTO, nil, parse_properties)
|
||||
end
|
||||
|
||||
---Move the browser to a particular point in the browser history.
|
||||
---The history is a linear list of visited directories from oldest to newest.
|
||||
---If the user changes directories while the current history position is not the head of the list,
|
||||
---any later directories get cleared and the new directory becomes the new head.
|
||||
---@param pos number The history index to move to. Clamped to [1,history_length]
|
||||
---@return number|false # The index actually moved to after clamping. Returns -1 if the index was invalid (can occur if history is empty or disabled)
|
||||
function directory_movement.goto_history(pos)
|
||||
if type(pos) ~= "number" then return false end
|
||||
|
||||
if pos < 1 then pos = 1
|
||||
elseif pos > g.history.size then pos = g.history.size end
|
||||
if not g.history.list[pos] then return false end
|
||||
|
||||
g.history.position = pos
|
||||
directory_movement.goto_directory(g.history.list[pos])
|
||||
return pos
|
||||
end
|
||||
|
||||
--loads the root list
|
||||
function directory_movement.goto_root()
|
||||
msg.verbose('jumping to root')
|
||||
return directory_movement.goto_directory("")
|
||||
end
|
||||
|
||||
--switches to the directory of the currently playing file
|
||||
function directory_movement.goto_current_dir()
|
||||
msg.verbose('jumping to current directory')
|
||||
return directory_movement.goto_directory(g.current_file.directory)
|
||||
end
|
||||
|
||||
--moves up a directory
|
||||
function directory_movement.up_dir()
|
||||
if g.state.directory == '' then return end
|
||||
|
||||
local cached_parent_dir = g.directory_stack.stack[g.directory_stack.position - 1]
|
||||
if cached_parent_dir then
|
||||
g.directory_stack.position = g.directory_stack.position - 1
|
||||
return directory_movement.goto_directory(cached_parent_dir, NavType.UP)
|
||||
end
|
||||
|
||||
local parent_dir = g.state.directory:match("^(.-/+)[^/]+/*$") or ""
|
||||
|
||||
if o.skip_protocol_schemes and parent_dir:find("^(%a[%w+-.]*)://$") then
|
||||
return directory_movement.goto_root()
|
||||
end
|
||||
|
||||
directory_stack_prepend(parent_dir)
|
||||
return directory_movement.goto_directory(parent_dir, NavType.UP)
|
||||
end
|
||||
|
||||
--moves down a directory
|
||||
function directory_movement.down_dir()
|
||||
local current = g.state.list[g.state.selected]
|
||||
if not current or not fb_utils.parseable_item(current) then return end
|
||||
|
||||
local directory, redirected = fb_utils.get_new_directory(current, g.state.directory)
|
||||
directory_stack_append(directory)
|
||||
return directory_movement.goto_directory(directory, redirected and NavType.REDIRECT or NavType.DOWN)
|
||||
end
|
||||
|
||||
--moves backwards through the directory history
|
||||
function directory_movement.back_history()
|
||||
msg.debug('moving backwards in history to', g.history.list[g.history.position-1])
|
||||
if g.history.position == 1 then return end
|
||||
directory_movement.goto_history(g.history.position - 1)
|
||||
end
|
||||
|
||||
--moves forward through the history
|
||||
function directory_movement.forwards_history()
|
||||
msg.debug('moving forwards in history to', g.history.list[g.history.position+1])
|
||||
if g.history.position == g.history.size then return end
|
||||
directory_movement.goto_history(g.history.position + 1)
|
||||
end
|
||||
|
||||
return directory_movement
|
||||
@@ -0,0 +1,210 @@
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
local g = require 'modules.globals'
|
||||
local fb_utils = require 'modules.utils'
|
||||
local cursor = require 'modules.navigation.cursor'
|
||||
local ass = require 'modules.ass'
|
||||
|
||||
local parse_state_API = require 'modules.apis.parse-state'
|
||||
|
||||
---@class scanning
|
||||
local scanning = {}
|
||||
|
||||
---@enum NavigationType
|
||||
local NavType = {
|
||||
DOWN = 1,
|
||||
UP = -1,
|
||||
REDIRECT = 2,
|
||||
GOTO = 3,
|
||||
RESCAN = 4,
|
||||
}
|
||||
|
||||
scanning.NavType = NavType
|
||||
|
||||
---@param directory_stack? boolean
|
||||
local function clear_non_adjacent_state(directory_stack)
|
||||
g.state.directory_label = nil
|
||||
if directory_stack then
|
||||
g.directory_stack.stack = {g.state.directory}
|
||||
g.directory_stack.position = 1
|
||||
end
|
||||
end
|
||||
|
||||
---parses the given directory or defers to the next parser if nil is returned
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param index number
|
||||
---@return List?
|
||||
---@return Opts?
|
||||
function scanning.choose_and_parse(directory, index)
|
||||
msg.debug(("finding parser for %q"):format(directory))
|
||||
---@type Parser, List?, Opts?
|
||||
local parser, list, opts
|
||||
local parse_state = g.parse_states[coroutine.running() or ""]
|
||||
while list == nil and not parse_state.already_deferred and index <= #g.parsers do
|
||||
parser = g.parsers[index]
|
||||
if parser:can_parse(directory, parse_state) then
|
||||
msg.debug("attempting parser:", parser:get_id())
|
||||
list, opts = parser:parse(directory, parse_state)
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
if not list then return nil, {} end
|
||||
|
||||
msg.debug("list returned from:", parser:get_id())
|
||||
opts = opts or {}
|
||||
if list then opts.id = opts.id or parser:get_id() end
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---Sets up the parse_state table and runs the parse operation.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param parse_state_template ParseStateTemplate
|
||||
---@return List|nil
|
||||
---@return Opts
|
||||
local function run_parse(directory, parse_state_template)
|
||||
msg.verbose(("scanning files in %q"):format(directory))
|
||||
|
||||
---@type ParseStateFields
|
||||
local parse_state = {
|
||||
source = parse_state_template.source,
|
||||
directory = directory,
|
||||
properties = parse_state_template.properties or {}
|
||||
}
|
||||
|
||||
local co = coroutine.running()
|
||||
g.parse_states[co] = fb_utils.set_prototype(parse_state, parse_state_API) --[[@as ParseState]]
|
||||
|
||||
local list, opts = scanning.choose_and_parse(directory, 1)
|
||||
|
||||
if list == nil then return msg.debug("no successful parsers found"), {} end
|
||||
opts = opts or {}
|
||||
opts.parser = g.parsers[opts.id]
|
||||
|
||||
if not opts.filtered then fb_utils.filter(list) end
|
||||
if not opts.sorted then fb_utils.sort(list) end
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---Returns the contents of the given directory using the given parse state.
|
||||
---If a coroutine has already been used for a parse then create a new coroutine so that
|
||||
---the every parse operation has a unique thread ID.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param parse_state ParseStateTemplate
|
||||
---@return List|nil
|
||||
---@return Opts
|
||||
function scanning.scan_directory(directory, parse_state)
|
||||
local co = fb_utils.coroutine.assert("scan_directory must be executed from within a coroutine - aborting scan "..utils.to_string(parse_state))
|
||||
if not g.parse_states[co] then return run_parse(directory, parse_state) end
|
||||
|
||||
--if this coroutine is already is use by another parse operation then we create a new
|
||||
--one and hand execution over to that
|
||||
---@async
|
||||
local new_co = coroutine.create(function()
|
||||
fb_utils.coroutine.resume_err(co, run_parse(directory, parse_state))
|
||||
end)
|
||||
|
||||
--queue the new coroutine on the mpv event queue
|
||||
mp.add_timeout(0, function()
|
||||
local success, err = coroutine.resume(new_co)
|
||||
if not success then
|
||||
fb_utils.traceback(err, new_co)
|
||||
fb_utils.coroutine.resume_err(co)
|
||||
end
|
||||
end)
|
||||
return g.parse_states[co]:yield()
|
||||
end
|
||||
|
||||
---Sends update requests to the different parsers.
|
||||
---@async
|
||||
---@param moving_adjacent? number|boolean
|
||||
---@param parse_properties? ParseProperties
|
||||
local function update_list(moving_adjacent, parse_properties)
|
||||
msg.verbose('opening directory: ' .. g.state.directory)
|
||||
|
||||
g.state.selected = 1
|
||||
g.state.selection = {}
|
||||
|
||||
local directory = g.state.directory
|
||||
local list, opts = scanning.scan_directory(g.state.directory, { source = "browser", properties = parse_properties })
|
||||
|
||||
--if the running coroutine isn't the one stored in the state variable, then the user
|
||||
--changed directories while the coroutine was paused, and this operation should be aborted
|
||||
if coroutine.running() ~= g.state.co then
|
||||
msg.verbose(g.ABORT_ERROR.msg)
|
||||
msg.debug("expected:", g.state.directory, "received:", directory)
|
||||
return
|
||||
end
|
||||
|
||||
--apply fallbacks if the scan failed
|
||||
if not list then
|
||||
msg.warn("could not read directory", g.state.directory)
|
||||
list, opts = {}, {}
|
||||
opts.empty_text = g.style.warning..'Error: could not parse directory'
|
||||
end
|
||||
|
||||
g.state.list = list
|
||||
g.state.parser = opts.parser
|
||||
|
||||
--setting custom options from parsers
|
||||
g.state.directory_label = opts.directory_label
|
||||
g.state.empty_text = opts.empty_text or g.state.empty_text
|
||||
|
||||
--we assume that directory is only changed when redirecting to a different location
|
||||
--therefore we need to change the `moving_adjacent` flag and clear some state values
|
||||
if opts.directory then
|
||||
g.state.directory = opts.directory
|
||||
moving_adjacent = false
|
||||
clear_non_adjacent_state(true)
|
||||
end
|
||||
|
||||
if opts.selected_index then
|
||||
g.state.selected = opts.selected_index or g.state.selected
|
||||
if g.state.selected > #g.state.list then g.state.selected = #g.state.list
|
||||
elseif g.state.selected < 1 then g.state.selected = 1 end
|
||||
end
|
||||
|
||||
if moving_adjacent then cursor.select_prev_directory()
|
||||
else cursor.select_playing_item() end
|
||||
g.state.prev_directory = g.state.directory
|
||||
end
|
||||
|
||||
---rescans the folder and updates the list.
|
||||
---@param nav_type? NavigationType
|
||||
---@param cb? function
|
||||
---@param parse_properties? ParseProperties
|
||||
---@return thread # The coroutine for the triggered parse operation. May be aborted early if directory is in the cache.
|
||||
function scanning.rescan(nav_type, cb, parse_properties)
|
||||
if nav_type == nil then nav_type = NavType.RESCAN end
|
||||
|
||||
--we can only make assumptions about the directory label when moving from adjacent directories
|
||||
if nav_type == NavType.GOTO or nav_type == NavType.REDIRECT then
|
||||
clear_non_adjacent_state(nav_type == NavType.GOTO)
|
||||
end
|
||||
|
||||
g.state.empty_text = "~"
|
||||
g.state.list = {}
|
||||
cursor.disable_select_mode()
|
||||
ass.update_ass()
|
||||
|
||||
--the directory is always handled within a coroutine to allow addons to
|
||||
--pause execution for asynchronous operations
|
||||
---@async
|
||||
local co = fb_utils.coroutine.queue(function()
|
||||
update_list(nav_type, parse_properties)
|
||||
if g.state.empty_text == "~" then g.state.empty_text = "empty directory" end
|
||||
|
||||
ass.update_ass()
|
||||
if cb then fb_utils.coroutine.run(cb) end
|
||||
end)
|
||||
|
||||
g.state.co = co
|
||||
return co
|
||||
end
|
||||
|
||||
|
||||
return scanning
|
||||
Reference in New Issue
Block a user