Last active
July 2, 2026 02:57
-
-
Save HedioKojima/fdbfdd73570650b01c809afb5ae7829b to your computer and use it in GitHub Desktop.
mpv WebDAV lua 由Qwen3.7-Max编写的osd版 功能灵感来自https://github.com/Ladersbt/mpv-config/blob/master/scripts/uosc_webdav.lua 🖖
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local mp = require('mp') | |
| local msg = require('mp.msg') | |
| local options = require('mp.options') | |
| local utils = require('mp.utils') | |
| local commandv = mp.commandv | |
| local command_native_async = mp.command_native_async | |
| local set_property = mp.set_property | |
| local get_property = mp.get_property | |
| local osd_message = mp.osd_message | |
| -- ================= 默认配置 ================= | |
| local opts = { | |
| url = "http://192.168.1.100:8080/dav/", | |
| user = "admin", | |
| pass = "password", | |
| max_lines = 15, | |
| font_size = 32, | |
| color_normal = "FFFFFF", | |
| color_active = "0CD8FF", | |
| header_color = "AAAAAA", | |
| delete_mode_color = "FF5555", | |
| video_exts = "mp4,mkv,avi,mov,wmv,flv,webm,m2ts,ts,rmvb,m4v,iso,vob", | |
| sub_exts = "srt,ass,ssa,vtt,sup,sub,idx,smi,lrc", | |
| slang = "chs,sc,zh-hans,zh-cn,jptc,cht,tc,zh-hant,zh-hk,zh-tw,chi,zho,zh,eng,en", | |
| cache_enabled = true, | |
| append_following_files = true, | |
| video_only = true, | |
| sync_playlist_sort = false, | |
| } | |
| options.read_options(opts, "webdav") | |
| set_property("slang", opts.slang) | |
| -- ================= 工具函数 ================= | |
| local function normalize_url(url) | |
| if not url or url == "" then return "" end | |
| if not url:match("/$") then url = url .. "/" end | |
| return url | |
| end | |
| local function split_csv(s) | |
| local ret = {} | |
| for part in tostring(s or ""):gmatch("([^,%s]+)") do ret[#ret + 1] = part:lower() end | |
| return ret | |
| end | |
| local function url_encode(str) | |
| if not str then return "" end | |
| return str:gsub("([^%w%-%.%_%~])", function(c) return string.format("%%%02X", string.byte(c)) end) | |
| end | |
| local function url_decode(str) | |
| if not str then return "" end | |
| return str:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) | |
| end | |
| local function encode_webdav_path(path) | |
| local decoded = url_decode(path) | |
| return decoded:gsub("([^%w%-%.%_%~/])", function(c) return string.format("%%%02X", string.byte(c)) end) | |
| end | |
| local function xml_decode(str) | |
| if not str then return "" end | |
| return str:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub(""", '"'):gsub("'", "'") | |
| end | |
| local function ass_escape(str) | |
| if not str then return "" end | |
| return str:gsub("\\", "\\\\"):gsub("{", "\\{"):gsub("}", "\\}") | |
| end | |
| local function utf8_chars(s) | |
| local chars = {} | |
| if not s then return chars end | |
| for char in s:gmatch("[%z\1-\127\194-\244][\128-\191]*") do chars[#chars + 1] = char end | |
| return chars | |
| end | |
| local function truncate_middle(s, max_chars) | |
| if not s then return "" end | |
| local chars = utf8_chars(s) | |
| if #chars <= max_chars then return s end | |
| if max_chars <= 3 then return "..." end | |
| local head = math.floor((max_chars - 3) / 2) | |
| local tail = max_chars - 3 - head | |
| local res = {} | |
| for i = 1, head do res[#res + 1] = chars[i] end | |
| res[#res + 1] = "..." | |
| for i = #chars - tail + 1, #chars do res[#res + 1] = chars[i] end | |
| return table.concat(res) | |
| end | |
| local function utf8_backspace(s) | |
| if not s or s == "" then return "" end | |
| return s:match("^(.-)[%z\1-\127\194-\244][\128-\191]*$") or "" | |
| end | |
| local function natural_compare(a, b) | |
| local function split(s) | |
| local t = {} | |
| for text, num in s:lower():gmatch("(%D*)(%d*)") do | |
| if text ~= "" then t[#t + 1] = text end | |
| if num ~= "" then t[#t + 1] = tonumber(num) end | |
| end | |
| return t | |
| end | |
| local ta, tb = split(a), split(b) | |
| for i = 1, math.max(#ta, #tb) do | |
| local va, vb = ta[i], tb[i] | |
| if va == nil then return true end | |
| if vb == nil then return false end | |
| if type(va) ~= type(vb) then return tostring(va) < tostring(vb) end | |
| if va ~= vb then return va < vb end | |
| end | |
| return false | |
| end | |
| local month_map = { Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, Oct=10, Nov=11, Dec=12 } | |
| local function parse_lastmod(s) | |
| if not s or s == "" then return 0 end | |
| local day, mon, year, h, m = s:match("(%d+)%s+(%a+)%s+(%d+)%s+(%d+):(%d+):") | |
| if not day then return 0 end | |
| return (tonumber(year) or 0) * 100000000 + (month_map[mon] or 0) * 1000000 + tonumber(day) * 10000 + tonumber(h) * 100 + tonumber(m) | |
| end | |
| local function wipe(t) for k in pairs(t) do t[k] = nil end end | |
| -- ================= 初始化常量 ================= | |
| opts.url = normalize_url(opts.url) | |
| local protocol, domain = opts.url:match("^(https?://)([^/]+)") | |
| if not protocol or not domain then msg.error("webdav.lua: opts.url 格式错误"); return end | |
| local auth_prefix = string.format("%s%s:%s@%s", protocol, url_encode(opts.user), url_encode(opts.pass), domain) | |
| local video_exts = {} | |
| for _, ext in ipairs(split_csv(opts.video_exts)) do video_exts[ext] = true end | |
| local sub_exts = {} | |
| for _, ext in ipairs(split_csv(opts.sub_exts)) do sub_exts[ext] = true end | |
| -- ================= 状态管理 ================= | |
| local overlay = mp.create_osd_overlay("ass-events") | |
| local is_menu_open = false | |
| local current_items = {} | |
| local filtered_items = {} | |
| local cursor_pos = 1 | |
| local current_loaded_url = "" | |
| local last_visited_url = opts.url | |
| local search_pattern = "" | |
| local active_bindings = {} | |
| local dir_cache = {} | |
| local request_serial = 0 | |
| local sub_map = {} | |
| local is_delete_mode = false | |
| local selected_items = {} | |
| local dir_sort = {} | |
| local dir_cursor = {} | |
| local sort_labels = { name_asc = "名称 A→Z", name_desc = "名称 Z→A", time_desc = "时间 新→旧", time_asc = "时间 旧→新" } | |
| -- 核心控制函数前向声明 (彻底解决 Lua 循环依赖与 nil value 陷阱) | |
| local close_menu, toggle_menu | |
| local enter_search_mode, exit_search_mode | |
| local bind_nav_mode, bind_search_mode | |
| local is_search_mode = false | |
| local function get_sort_mode() return dir_sort[current_loaded_url] or "name_asc" end | |
| local function set_sort_mode(m) dir_sort[current_loaded_url] = m end | |
| local function save_cursor_for_dir() if current_loaded_url ~= "" then dir_cursor[current_loaded_url] = cursor_pos end end | |
| -- ================= 数据处理 ================= | |
| local function apply_sort() | |
| local m = get_sort_mode() | |
| table.sort(current_items, function(a, b) | |
| local pa = a.type == "up" and 1 or (a.type == "dir" and 2 or 3) | |
| local pb = b.type == "up" and 1 or (b.type == "dir" and 2 or 3) | |
| if pa ~= pb then return pa < pb end | |
| if a.type == "up" then return false end | |
| if m == "name_desc" then return natural_compare(b.name, a.name) | |
| elseif m == "time_desc" then | |
| local ta, tb = parse_lastmod(a.lastmod), parse_lastmod(b.lastmod) | |
| if ta ~= tb then return ta > tb end; return natural_compare(a.name, b.name) | |
| elseif m == "time_asc" then | |
| local ta, tb = parse_lastmod(a.lastmod), parse_lastmod(b.lastmod) | |
| if ta ~= tb then return ta < tb end; return natural_compare(a.name, b.name) | |
| else return natural_compare(a.name, b.name) end | |
| end) | |
| end | |
| local function apply_filter() | |
| wipe(filtered_items) | |
| local p = search_pattern:lower() | |
| for _, v in ipairs(current_items) do | |
| if v.type == "dir" or v.type == "up" or not opts.video_only or v.is_video then | |
| if p == "" or v.name:lower():find(p, 1, true) then filtered_items[#filtered_items + 1] = v end | |
| end | |
| end | |
| if #filtered_items == 0 then cursor_pos = 1 else cursor_pos = math.max(1, math.min(cursor_pos, #filtered_items)) end | |
| end | |
| local function parse_and_build(stdout, target_url) | |
| local items = {} | |
| local target_path = target_url:match("https?://[^/]+(/.*)") or "/" | |
| local norm_current = url_decode(target_path):gsub("/+$", "") | |
| if target_url ~= opts.url then items[#items + 1] = { type = "up", title = "↩️ .. (返回上一层)", name = ".." } end | |
| stdout = stdout:gsub("\r", "") | |
| for block in stdout:gmatch("<[^>]*[Rr]esponse[^>]*>(.-)</[^>]*[Rr]esponse>") do | |
| local raw_href = block:match("<[^>]*[Hh]ref[^>]*>([^<]+)</[^>]*[Hh]ref>") | |
| if raw_href then | |
| local lastmod_str = block:match("<[^>]*[Gg]etlastmodified[^>]*>([^<]+)</[^>]*[Gg]etlastmodified>") or "" | |
| raw_href = xml_decode(raw_href) | |
| local href_path = raw_href:match("https?://[^/]+(/.*)") or raw_href | |
| local decoded_href = url_decode(href_path) | |
| local norm_decoded = decoded_href:gsub("/+$", "") | |
| if norm_decoded ~= norm_current then | |
| local is_dir = block:match("<[^>]*[Rr]esourcetype[^>]*>.-<[Cc]ollection") ~= nil | |
| if not is_dir then is_dir = raw_href:sub(-1) == "/" end | |
| local name = decoded_href:match("([^/]+)/?$") or decoded_href | |
| if name == "" then name = "/" end | |
| local safe_url = encode_webdav_path(raw_href) | |
| if is_dir then | |
| items[#items + 1] = { type = "dir", name = name, title = "📁 " .. name, url = protocol .. domain .. safe_url, delete_url = protocol .. domain .. safe_url, lastmod = lastmod_str } | |
| else | |
| local ext = name:match("%.([^%.]+)$") | |
| local ext_lower = ext and ext:lower() | |
| items[#items + 1] = { type = "file", name = name, title = (ext_lower and video_exts[ext_lower] and "🎬 " or "📄 ") .. name, play_url = auth_prefix .. safe_url, delete_url = protocol .. domain .. safe_url, lastmod = lastmod_str, is_video = ext_lower and video_exts[ext_lower], is_sub = ext_lower and sub_exts[ext_lower] } | |
| end | |
| end | |
| end | |
| end | |
| apply_sort() | |
| return items | |
| end | |
| local function build_sub_map(items) | |
| wipe(sub_map) | |
| local subs = {} | |
| for _, item in ipairs(items) do | |
| if item.type == "file" and item.is_sub then | |
| local stem = (item.name:match("^(.+)%.[^%.]+$") or item.name):lower() | |
| subs[#subs + 1] = { stem = stem, url = item.play_url, title = item.name } | |
| end | |
| end | |
| for _, item in ipairs(items) do | |
| if item.type == "file" and item.is_video then | |
| local vstem = (item.name:match("^(.+)%.[^%.]+$") or item.name):lower() | |
| local matched = {} | |
| for _, sub in ipairs(subs) do | |
| if sub.stem:find(vstem, 1, true) then matched[#matched + 1] = { url = sub.url, title = sub.title } end | |
| end | |
| if #matched > 0 then sub_map[item.play_url] = matched end | |
| end | |
| end | |
| end | |
| -- ================= UI 渲染 (集成截断与防遮挡布局) ================= | |
| local function render_menu() | |
| if not is_menu_open then overlay:remove(); return end | |
| if #filtered_items == 0 then | |
| overlay.data = string.format("{\\q2\\fs%d\\c&H%s&}WebDAV: 目录为空或正在加载...", opts.font_size, opts.header_color) | |
| overlay:update(); return | |
| end | |
| -- 【调整】路径截断限制为 45 | |
| local path_text = url_decode((current_loaded_url:match("https?://[^/]+(/.*)") or "/")) | |
| local display_path = truncate_middle(path_text, 45) | |
| local title = string.format("{\\q2\\fs%d\\c&H%s&}WebDAV: %s [%s]", opts.font_size, opts.header_color, ass_escape(display_path), sort_labels[get_sort_mode()]) | |
| if is_search_mode then | |
| title = title .. string.format(" {\\c&H00FFFF&}🔍 [Search: %s_]", ass_escape(search_pattern)) | |
| elseif search_pattern ~= "" then | |
| title = title .. string.format(" {\\c&H00FFFF&}[Filter: %s]", ass_escape(search_pattern)) | |
| else | |
| title = title .. string.format(" {\\c&H888888&}[/ 搜索 | s 排序 | d 删除]") | |
| end | |
| local start_idx = math.max(1, cursor_pos - math.floor(opts.max_lines / 2)) | |
| local end_idx = math.min(#filtered_items, start_idx + opts.max_lines - 1) | |
| if end_idx - start_idx < opts.max_lines - 1 and #filtered_items >= opts.max_lines then start_idx = math.max(1, end_idx - opts.max_lines + 1) end | |
| -- 【调整】顶部强制预留 4 行空白,彻底防止 mpv 核心 OSD 遮挡标题和列表 | |
| local lines = { "\\N", "\\N", "\\N", "\\N", title, "\\N" } | |
| for i = start_idx, end_idx do | |
| local item = filtered_items[i] | |
| local is_selected = is_delete_mode and selected_items[item.delete_url] | |
| local prefix = is_delete_mode and (is_selected and "▶☑ " or "☐ ") or ((i == cursor_pos) and "● " or " ") | |
| local color = (i == cursor_pos or is_selected) and (is_delete_mode and opts.delete_mode_color or opts.color_active) or opts.color_normal | |
| local icon = "" | |
| if item.type == "up" then icon = "↩️ " | |
| elseif item.type == "dir" then icon = "📁 " | |
| elseif item.is_video then icon = "🎬 " | |
| else icon = "📄 " end | |
| -- 【调整】文件名截断限制放宽至 90 | |
| local truncated_name = truncate_middle(item.name, 90) | |
| local display_name = "" | |
| if search_pattern ~= "" and item.type ~= "up" then | |
| local p = search_pattern:lower() | |
| local name_lower = truncated_name:lower() | |
| local s, e = name_lower:find(p, 1, true) | |
| if s then | |
| local part1 = ass_escape(truncated_name:sub(1, s-1)) | |
| local part2 = ass_escape(truncated_name:sub(s, e)) | |
| local part3 = ass_escape(truncated_name:sub(e+1)) | |
| display_name = ass_escape(icon) .. part1 .. "{\\c&H00FFFF&}" .. part2 .. "{\\c&H" .. color .. "&}" .. part3 | |
| else | |
| display_name = ass_escape(icon) .. ass_escape(truncated_name) | |
| end | |
| else | |
| display_name = ass_escape(icon) .. ass_escape(truncated_name) | |
| end | |
| lines[#lines + 1] = string.format("{\\c&H%s&}%s%s\\N", color, prefix, display_name) | |
| end | |
| overlay.data = table.concat(lines) | |
| overlay:update() | |
| end | |
| -- ================= 异步网络请求 ================= | |
| local function open_webdav_url(target_url, force_refresh) | |
| target_url = normalize_url(target_url) | |
| if target_url == "" then return end | |
| save_cursor_for_dir() | |
| current_loaded_url = target_url | |
| last_visited_url = target_url | |
| is_delete_mode = false | |
| search_pattern = "" | |
| wipe(selected_items) | |
| if opts.cache_enabled and dir_cache[target_url] and not force_refresh then | |
| current_items = {}; for _, v in ipairs(dir_cache[target_url]) do current_items[#current_items + 1] = v end | |
| cursor_pos = dir_cursor[target_url] or 1 | |
| build_sub_map(current_items) | |
| apply_sort(); apply_filter(); render_menu() | |
| return | |
| end | |
| osd_message("正在获取列表...", 1) | |
| render_menu() | |
| request_serial = request_serial + 1 | |
| local serial = request_serial | |
| command_native_async({ | |
| name = "subprocess", playback_only = false, capture_stdout = true, | |
| args = { "curl", "-s", "-k", "-X", "PROPFIND", "-u", opts.user .. ":" .. opts.pass, "-H", "Depth: 1", target_url } | |
| }, function(success, result, err) | |
| if not is_menu_open or serial ~= request_serial then return end | |
| if not success or not result or result.status ~= 0 then osd_message("连接失败", 3); return end | |
| current_items = parse_and_build(result.stdout or "", target_url) | |
| build_sub_map(current_items) | |
| if opts.cache_enabled then | |
| dir_cache[target_url] = {} | |
| for _, v in ipairs(current_items) do dir_cache[target_url][#dir_cache[target_url] + 1] = v end | |
| end | |
| cursor_pos = dir_cursor[target_url] or 1 | |
| apply_filter(); render_menu() | |
| end) | |
| end | |
| -- ================= 播放逻辑 ================= | |
| local function play_file_with_playlist(item) | |
| if not opts.append_following_files then | |
| commandv("loadfile", item.play_url, "replace") | |
| osd_message("🎬 播放单文件 (未加载列表)", 2) | |
| return | |
| end | |
| local playable = {} | |
| for _, v in ipairs(current_items) do | |
| if v.type == "file" and (not opts.video_only or v.is_video) then playable[#playable + 1] = v end | |
| end | |
| if not opts.sync_playlist_sort then table.sort(playable, function(a, b) return natural_compare(a.name, b.name) end) end | |
| if #playable == 0 then return end | |
| local target_pos = 0 | |
| for i, v in ipairs(playable) do if v.play_url == item.play_url then target_pos = i - 1; break end end | |
| commandv("loadfile", playable[1].play_url, "replace") | |
| for i = 2, #playable do commandv("loadfile", playable[i].play_url, "append") end | |
| commandv("playlist-play-index", target_pos) | |
| local mode_hint = opts.sync_playlist_sort and ("继承: " .. sort_labels[get_sort_mode()]) or "名称 A→Z" | |
| osd_message(string.format("🎬 播放列表 %d 个视频 [%s]", #playable, mode_hint), 3) | |
| end | |
| mp.register_event("file-loaded", function() | |
| local path = get_property("path") | |
| if path and sub_map[path] then | |
| for _, sub in ipairs(sub_map[path]) do commandv("sub-add", sub.url, "auto", sub.title) end | |
| end | |
| end) | |
| -- ================= 删除核心逻辑 ================= | |
| local function execute_delete_queue(queue, index, total, callback) | |
| if index > total then callback(true); return end | |
| local item = queue[index] | |
| local target_url = item.delete_url | |
| if item.type == "dir" and not target_url:match("/$") then target_url = target_url .. "/" end | |
| osd_message(string.format("🗑️ 正在删除 %d/%d: %s", index, total, item.name), 1) | |
| command_native_async({ | |
| name = "subprocess", playback_only = false, | |
| args = { "curl", "-s", "-k", "-X", "DELETE", "-u", opts.user .. ":" .. opts.pass, target_url } | |
| }, function(success, result, err) | |
| if not success or not result or result.status ~= 0 then | |
| osd_message("❌ 删除失败: " .. item.name, 3) | |
| callback(false); return | |
| end | |
| execute_delete_queue(queue, index + 1, total, callback) | |
| end) | |
| end | |
| local function toggle_delete_mode() | |
| is_delete_mode = not is_delete_mode | |
| wipe(selected_items) | |
| osd_message(is_delete_mode and "🗑️ 删除模式 (Space选中, A全选, X执行, ESC退出)" or "退出删除模式", 2) | |
| render_menu() | |
| end | |
| local function toggle_select_item() | |
| if not is_delete_mode then return end | |
| local item = filtered_items[cursor_pos] | |
| if not item or item.type == "up" then return end | |
| local key = item.delete_url | |
| if selected_items[key] then selected_items[key] = nil else selected_items[key] = item end | |
| render_menu() | |
| end | |
| local function select_all() | |
| if not is_delete_mode then return end | |
| local all_selected = true | |
| for _, item in ipairs(filtered_items) do | |
| if item.type ~= "up" and not selected_items[item.delete_url] then all_selected = false; break end | |
| end | |
| wipe(selected_items) | |
| if not all_selected then | |
| for _, item in ipairs(filtered_items) do | |
| if item.type ~= "up" then selected_items[item.delete_url] = item end | |
| end | |
| end | |
| render_menu() | |
| end | |
| local function confirm_and_delete() | |
| if not is_delete_mode then return end | |
| local queue = {} | |
| for _, item in pairs(selected_items) do queue[#queue + 1] = item end | |
| if #queue == 0 then osd_message("未选择任何文件", 1); return end | |
| osd_message(string.format("⚠️ 确认删除 %d 个项目?(再次按 X 确认, ESC取消)", #queue), 5) | |
| local confirm_bind = "wd-confirm-delete" | |
| mp.add_forced_key_binding("x", confirm_bind, function() | |
| mp.remove_key_binding(confirm_bind) | |
| is_delete_mode = false; wipe(selected_items) | |
| execute_delete_queue(queue, 1, #queue, function(success) | |
| if success then osd_message("✅ 删除完成", 2); open_webdav_url(current_loaded_url, true) | |
| else render_menu() end | |
| end) | |
| end) | |
| mp.add_timeout(5, function() mp.remove_key_binding(confirm_bind) end) | |
| end | |
| -- ================= 搜索输入路由 ================= | |
| local function handle_search_input(input) | |
| if input.event == "up" then return end | |
| local key_text = input.key_text | |
| if key_text and key_text ~= "" and key_text:byte(1) >= 32 then | |
| search_pattern = search_pattern .. key_text | |
| apply_filter() | |
| render_menu() | |
| end | |
| end | |
| local function backspace_search() | |
| if search_pattern ~= "" then | |
| search_pattern = utf8_backspace(search_pattern) | |
| apply_filter() | |
| render_menu() | |
| else | |
| exit_search_mode() | |
| end | |
| end | |
| local function clear_search() | |
| search_pattern = "" | |
| apply_filter() | |
| render_menu() | |
| end | |
| -- ================= 菜单控制 (赋值实现,彻底打通闭包上值) ================= | |
| close_menu = function() | |
| is_menu_open = false; is_delete_mode = false; is_search_mode = false; wipe(selected_items) | |
| request_serial = request_serial + 1 | |
| overlay:remove() | |
| for _, name in ipairs(active_bindings) do mp.remove_key_binding(name) end | |
| wipe(active_bindings) | |
| end | |
| -- ================= 核心:模态按键绑定系统 ================= | |
| local function bind(key, name, fn, opt) | |
| mp.add_forced_key_binding(key, name, fn, opt) | |
| active_bindings[#active_bindings + 1] = name | |
| end | |
| bind_nav_mode = function() | |
| for _, name in ipairs(active_bindings) do mp.remove_key_binding(name) end | |
| wipe(active_bindings) | |
| bind("UP", "wd-up", function() cursor_pos = cursor_pos - 1; if cursor_pos < 1 then cursor_pos = #filtered_items end; render_menu() end, {repeatable=true}) | |
| bind("DOWN", "wd-down", function() cursor_pos = cursor_pos + 1; if cursor_pos > #filtered_items then cursor_pos = 1 end; render_menu() end, {repeatable=true}) | |
| bind("RIGHT", "wd-enter", function() | |
| local item = filtered_items[cursor_pos] | |
| if not item then return end | |
| if is_delete_mode then toggle_select_item(); return end | |
| if item.type == "dir" then save_cursor_for_dir(); open_webdav_url(item.url) | |
| elseif item.type == "up" then open_webdav_url(current_loaded_url:match("^(.*/)[^/]+/?$") or opts.url) | |
| elseif item.type == "file" then play_file_with_playlist(item); close_menu() end | |
| end) | |
| bind("ESC", "wd-esc", function() | |
| if is_delete_mode then toggle_delete_mode() else close_menu() end | |
| end) | |
| bind("LEFT", "wd-back", function() open_webdav_url(current_loaded_url:match("^(.*/)[^/]+/?$") or opts.url) end) | |
| bind("s", "wd-sort", function() | |
| local modes = {"name_asc", "name_desc", "time_desc", "time_asc"} | |
| local cur = get_sort_mode() | |
| for i, m in ipairs(modes) do if m == cur then set_sort_mode(modes[(i % #modes) + 1]); break end end | |
| apply_sort(); apply_filter(); render_menu() | |
| osd_message("排序: " .. sort_labels[get_sort_mode()], 1) | |
| end) | |
| bind("d", "wd-del-mode", toggle_delete_mode) | |
| bind("SPACE", "wd-select", toggle_select_item) | |
| bind("a", "wd-select-all", select_all) | |
| bind("x", "wd-exec-del", confirm_and_delete) | |
| bind("/", "wd-enter-search", function() enter_search_mode() end) | |
| end | |
| bind_search_mode = function() | |
| for _, name in ipairs(active_bindings) do mp.remove_key_binding(name) end | |
| wipe(active_bindings) | |
| bind("UP", "wd-up", function() cursor_pos = cursor_pos - 1; if cursor_pos < 1 then cursor_pos = #filtered_items end; render_menu() end, {repeatable=true}) | |
| bind("DOWN", "wd-down", function() cursor_pos = cursor_pos + 1; if cursor_pos > #filtered_items then cursor_pos = 1 end; render_menu() end, {repeatable=true}) | |
| bind("RIGHT", "wd-search-enter", function() | |
| exit_search_mode() | |
| local item = filtered_items[cursor_pos] | |
| if not item then return end | |
| if item.type == "dir" then save_cursor_for_dir(); open_webdav_url(item.url) | |
| elseif item.type == "up" then open_webdav_url(current_loaded_url:match("^(.*/)[^/]+/?$") or opts.url) | |
| elseif item.type == "file" then play_file_with_playlist(item); close_menu() end | |
| end) | |
| bind("ESC", "wd-search-esc", function() exit_search_mode() end) | |
| bind("BS", "wd-search-bs", backspace_search, {repeatable=true}) | |
| bind("Ctrl+c", "wd-search-bs2", backspace_search, {repeatable=true}) | |
| bind("Ctrl+u", "wd-search-clear", clear_search) | |
| bind("ANY_UNICODE", "wd-search-input", handle_search_input, {repeatable=true, complex=true}) | |
| end | |
| enter_search_mode = function() | |
| is_search_mode = true | |
| bind_search_mode() | |
| render_menu() | |
| end | |
| exit_search_mode = function() | |
| is_search_mode = false | |
| bind_nav_mode() | |
| render_menu() | |
| end | |
| toggle_menu = function() | |
| if is_menu_open then close_menu(); return end | |
| is_menu_open = true | |
| search_pattern = "" | |
| bind_nav_mode() | |
| open_webdav_url(last_visited_url) | |
| end | |
| mp.add_key_binding("w", "toggle-webdav", toggle_menu) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| webdav.lua uosc版 可使用此处 https://github.com/Ladersbt/mpv-config/tree/master/scripts/uosc_webdav 🖖 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
您修改的很好,好多功能是我所想不到的,我让ai借用您许多新功能和思路重写了osd版的,感谢您的想法 🙏