first commit

This commit is contained in:
2025-06-14 20:26:14 +02:00
commit 1edfd60dbd
351 changed files with 34592 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
--------------------------------------------------------------------------------------------------------
--------------------------------Scroll/Select Implementation--------------------------------------------
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------
local g = require 'modules.globals'
local fb_utils = require 'modules.utils'
local ass = require 'modules.ass'
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]
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()
if g.state.prev_directory:find(g.state.directory, 1, true) == 1 then
local i = 1
while (g.state.list[i] and fb_utils.parseable_item(g.state.list[i])) do
if g.state.prev_directory:find(fb_utils.get_full_path(g.state.list[i]), 1, true) then
g.state.selected = i
return
end
i = i+1
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

View File

@@ -0,0 +1,86 @@
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 cache = require 'modules.cache'
local scanning = require 'modules.navigation.scanning'
local fb_utils = require 'modules.utils'
local directory_movement = {}
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
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 not g.state.hidden then ass.update_ass()
else g.state.flag_update = true end
end
--the base function for moving to a directory
function directory_movement.goto_directory(directory, moving_adjacent)
-- update cache to the lastest state values before changing the current directory
cache:add_current_state()
local current = g.state.list[g.state.selected]
g.state.directory = directory
if g.state.directory_label then
if moving_adjacent == 1 then
g.state.directory_label = g.state.directory_label..(current.label or current.name)
elseif moving_adjacent == -1 then
g.state.directory_label = string.match(g.state.directory_label, "^(.-/+)[^/]+/*$")
end
end
return scanning.rescan(moving_adjacent or false)
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()
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
return directory_movement.goto_directory(parent_dir, -1)
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)
return directory_movement.goto_directory(directory, not redirected and 1)
end
return directory_movement

View File

@@ -0,0 +1,182 @@
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 cache = require 'modules.cache'
local cursor = require 'modules.navigation.cursor'
local ass = require 'modules.ass'
local parse_state_API = require 'modules.apis.parse-state'
local function clear_non_adjacent_state()
g.state.directory_label = nil
cache:clear_traversal_stack()
end
--parses the given directory or defers to the next parser if nil is returned
local function choose_and_parse(directory, index)
msg.debug(("finding parser for %q"):format(directory))
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
local function run_parse(directory, parse_state)
msg.verbose(("scanning files in %q"):format(directory))
parse_state.directory = directory
local co = coroutine.running()
g.parse_states[co] = setmetatable(parse_state, { __index = parse_state_API })
local list, opts = choose_and_parse(directory, 1)
if list == nil then return msg.debug("no successful parsers found") end
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
local function parse_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
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
local function update_list(moving_adjacent)
msg.verbose('opening directory: ' .. g.state.directory)
g.state.selected = 1
g.state.selection = {}
--loads the current directry from the cache to save loading time
if cache:in_cache(g.state.directory) then
msg.verbose('found directory in cache')
cache:apply(g.state.directory)
g.state.prev_directory = g.state.directory
return
end
local directory = g.state.directory
local list, opts = parse_directory(g.state.directory, { source = "browser" })
--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 and cache:in_cache(g.state.prev_directory) then
--switches settings back to the previously opened directory
--to the user it will be like the directory never changed
msg.warn("could not read directory", g.state.directory)
cache:apply(g.state.prev_directory)
return
elseif not list then
--opens the root instead
msg.warn("could not read directory", g.state.directory, "redirecting to root")
list, opts = parse_directory("", { source = "browser" })
-- sets the directory redirect flag
opts.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()
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
--returns the coroutine for the new parse operation
local function rescan(moving_adjacent)
if moving_adjacent == nil then moving_adjacent = 0 end
--we can only make assumptions about the directory label when moving from adjacent directories
if not moving_adjacent then clear_non_adjacent_state() 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
g.state.co = fb_utils.coroutine.queue(function()
update_list(moving_adjacent)
if g.state.empty_text == "~" then g.state.empty_text = "empty directory" end
cache:append_history()
if type(moving_adjacent) == 'number' and moving_adjacent < 0 then cache:pop()
else cache:push() end
if not cache.traversal_stack[1] then cache:push() end
ass.update_ass()
end)
return g.state.co
end
return {
rescan = rescan,
scan_directory = parse_directory,
choose_and_parse = choose_and_parse,
}