Compare commits
5 Commits
9bc7887bd6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
9f0e7b422f
|
|||
|
a300158d53
|
|||
|
fd9d7859b8
|
|||
|
0ed904319d
|
|||
|
64922e1ae3
|
+3
-2
@@ -1,3 +1,6 @@
|
||||
**/.git
|
||||
.manager/*
|
||||
!.manager/.gitkeep
|
||||
cache/
|
||||
|
||||
files/*.log
|
||||
@@ -16,8 +19,6 @@ historybookmarks
|
||||
|
||||
*.log
|
||||
|
||||
**/.git/
|
||||
!/.git/
|
||||
.claude/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
@@ -16,3 +16,18 @@
|
||||
| ---------- | ------------------------------------- | ---------------------------------------------- |
|
||||
| alass | 字幕自动同步(autosubsync 脚本) | `paru -S alass` |
|
||||
| ffsubsync | 字幕自动同步(alass 的替代) | `pip install ffsubsync` |
|
||||
|
||||
## 字体
|
||||
|
||||
弹幕中可能出现 emoji, 因此需要使用支持的字体, 例如将 Symbola 加到 Noto Sans CJK SC 的末尾:
|
||||
|
||||
```xml
|
||||
<match target="pattern">
|
||||
<test name="family">
|
||||
<string>Noto Sans CJK SC</string>
|
||||
</test>
|
||||
<edit mode="append" name="family">
|
||||
<string>Symbola</string>
|
||||
</edit>
|
||||
</match>
|
||||
```
|
||||
|
||||
@@ -49,6 +49,32 @@ icc/ # ICC 色彩配置文件
|
||||
| Anime4K | 动画 | 低 |
|
||||
| SSIM | 低性能需求 | 低 |
|
||||
|
||||
其中:
|
||||
|
||||
- Ani4K / AniSD 着色器(`shaders/Ani4K/`)来自 [Sirosky/Upscale-Hub](https://github.com/Sirosky/Upscale-Hub)。
|
||||
- nnedi3 / ravu 着色器(`shaders/nnedi3/`,`shaders/ravu/`)来自 [mpv-prescalers](https://github.com/bjin/mpv-prescalers)。
|
||||
|
||||
## 外部依赖
|
||||
|
||||
见 [DEPENDENCIES.md](DEPENDENCIES.md)。
|
||||
|
||||
## 更新流程
|
||||
|
||||
1. 在 mpv 中按 `M` 触发 manager.lua,观察控制台输出,确认无 `FAILED` 条目。可能会有其他报错如 `[manager] Fehler: externes Repository manager existiert bereits.`,这是正常的。只需要确保不出现全大写的 `FAILED` 即可。
|
||||
2. 将 manager.lua 在 dest 目录产生的嵌套 `.git` 目录迁移到仓库内的 `.manager/`(避免根仓库误判为 submodule,幂等可重复执行):
|
||||
```bash
|
||||
REPO=$(git -C ~/.config/mpv rev-parse --show-toplevel)
|
||||
GITSTORE="$REPO/.manager"
|
||||
mkdir -p "$GITSTORE"
|
||||
find -L ~/.config/mpv -mindepth 2 -name .git -type d | while read gitdir; do
|
||||
dest="${gitdir%/.git}"
|
||||
rel="${dest#$REPO/}"
|
||||
name=$(echo "$rel" | tr '/' '-')
|
||||
mv "$gitdir" "$GITSTORE/$name"
|
||||
depth=$(echo "$rel" | tr -cd '/' | wc -c)
|
||||
ups=$(printf '../%.0s' $(seq 1 $((depth + 1))))
|
||||
echo "gitdir: ${ups}.manager/$name" > "$dest/.git"
|
||||
done
|
||||
```
|
||||
3. 重启 mpv,检查控制台有无 `unknown key` 或脚本加载失败的警告
|
||||
4. 若有 `unknown key` 警告,说明对应脚本的配置项发生变化,找 `script-opts/` 下同名 `.conf` 对照脚本源码更新
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,6 @@
|
||||
|
||||
##⇘⇘uosc 一级菜单:打开
|
||||
o script-message-to uosc open-file #menu: 打开 > 打开内置浏览器
|
||||
TAB script-message-to file_browser browse-files;script-message-to file_browser dynamic/reload;show-text '' #menu: 打开 > 打开 OSD 浏览器
|
||||
# script-message-to uosc playlist #menu: 打开 > 播放菜单
|
||||
# script-message-to uosc chapters #menu: 打开 > 章节菜单
|
||||
# script-message-to uosc editions #menu: 打开 > 版本菜单
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ RIGHT seek 5 #event:click
|
||||
RIGHT script-message-to evafast speedup #event:press
|
||||
RIGHT script-message-to evafast slowdown #event:release
|
||||
|
||||
TAB script-message-to file_browser browse-files;script-message-to file_browser dynamic/reload;show-text '' #event:click
|
||||
TAB script-message-to uosc open-file #event:click
|
||||
TAB script-message-to uosc toggle-ui #event:press
|
||||
TAB script-message-to uosc toggle-ui #event:release
|
||||
|
||||
|
||||
+119
-110
@@ -1,111 +1,120 @@
|
||||
[
|
||||
{
|
||||
"git":"https://github.com/po5/evafast",
|
||||
"branch":"rewrite",
|
||||
"whitelist":"%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/stax76/mpv-scripts",
|
||||
"branch":"main",
|
||||
"whitelist":"delete_current_file%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/mpv-scripts",
|
||||
"branch":"main",
|
||||
"blacklist":"license|%.md$|drcbox%.lua$|.-%-list%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/mpv-playlistmanager",
|
||||
"branch":"dev",
|
||||
"whitelist":"playlistmanager%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/mpv-sub-assrt",
|
||||
"whitelist":"%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/chapterskip",
|
||||
"branch":"dev",
|
||||
"whitelist":"%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/Eisa01_mpv-scripts",
|
||||
"branch":"dev",
|
||||
"whitelist":"undoredo%.lua$|simplehistory%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/dyphire/autosubsync-mpv",
|
||||
"branch":"v0.33_CM",
|
||||
"whitelist":"readme%.md$|%.lua$",
|
||||
"dest":"~~/scripts/autosubsync"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/christoph-heinrich/mpv-quality-menu",
|
||||
"whitelist":"quality%-menu%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/CogentRedTester/mpv-file-browser",
|
||||
"whitelist":"main%.lua$|readme%.md$|doc|modules",
|
||||
"dest":"~~/scripts/file-browser"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/CogentRedTester/mpv-sub-select",
|
||||
"whitelist":"sub%-select%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/CogentRedTester/mpv-scripts",
|
||||
"whitelist":"cycle%-commands%.lua$",
|
||||
"dest":"~~/scripts"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/CogentRedTester/mpv-file-browser",
|
||||
"whitelist":"favourites%.lua$|find%.lua$|home%-label%.lua$|url%-decode%.lua$|windir%.lua$|winroot%.lua$",
|
||||
"dest":"~~/script-modules/file-browser-addons"
|
||||
},
|
||||
{
|
||||
"git":"https://gist.github.com/igv/8a77e4eb8276753b54bb94c1c50c317e",
|
||||
"whitelist":"%.glsl$",
|
||||
"dest":"~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git":"https://gist.github.com/igv/a015fc885d5c22e6891820ad89555637",
|
||||
"whitelist":"%.glsl$",
|
||||
"dest":"~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git":"https://gist.github.com/igv/2364ffa6e81540f29cb7ab4c9bc05b6b",
|
||||
"whitelist":"%.glsl$",
|
||||
"dest":"~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git":"https://gist.github.com/igv/36508af3ffc84410fe39761d6969be10",
|
||||
"whitelist":"%.glsl$",
|
||||
"dest":"~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/Artoriuz/glsl-joint-bilateral",
|
||||
"branch":"main",
|
||||
"whitelist":"jointbilateral%.glsl$",
|
||||
"dest":"~~/shaders/other"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/Artoriuz/glsl-pixel-clipper",
|
||||
"branch":"main",
|
||||
"whitelist":"%.glsl$",
|
||||
"dest":"~~/shaders/other"
|
||||
},
|
||||
{
|
||||
"git":"https://github.com/bloc97/Anime4K",
|
||||
"whitelist":"%.md$|%.glsl$",
|
||||
"blacklist":"tensorflow",
|
||||
"dest":"~~/shaders/Anime4K"
|
||||
}]
|
||||
{
|
||||
"git": "https://github.com/po5/mpv_manager",
|
||||
"branch": "master",
|
||||
"whitelist": "manager%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/po5/evafast",
|
||||
"branch": "rewrite",
|
||||
"whitelist": "%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/stax76/mpv-scripts",
|
||||
"branch": "main",
|
||||
"whitelist": "delete_current_file%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/mpv-scripts",
|
||||
"branch": "main",
|
||||
"blacklist": "license|%.md$|drcbox%.lua$|.-%-list%.lua$|mpv-torrserver.lua",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/mpv-playlistmanager",
|
||||
"branch": "dev",
|
||||
"whitelist": "playlistmanager%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/mpv-sub-assrt",
|
||||
"whitelist": "%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/chapterskip",
|
||||
"branch": "dev",
|
||||
"whitelist": "%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/Eisa01_mpv-scripts",
|
||||
"branch": "dev",
|
||||
"whitelist": "undoredo%.lua$|simplehistory%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/dyphire/autosubsync-mpv",
|
||||
"branch": "v0.33_CM",
|
||||
"whitelist": "readme%.md$|%.lua$",
|
||||
"dest": "~~/scripts/autosubsync"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/christoph-heinrich/mpv-quality-menu",
|
||||
"whitelist": "quality%-menu%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/tomasklaen/uosc",
|
||||
"branch": "main",
|
||||
"whitelist": "src/uosc/",
|
||||
"dest": "~~/scripts/uosc"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/Tony15246/uosc_danmaku",
|
||||
"branch": "main",
|
||||
"blacklist": "^%.|^\"",
|
||||
"dest": "~~/scripts/uosc_danmaku"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/CogentRedTester/mpv-sub-select",
|
||||
"whitelist": "sub%-select%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/CogentRedTester/mpv-scripts",
|
||||
"whitelist": "cycle%-commands%.lua$",
|
||||
"dest": "~~/scripts"
|
||||
},
|
||||
{
|
||||
"git": "https://gist.github.com/igv/8a77e4eb8276753b54bb94c1c50c317e",
|
||||
"whitelist": "%.glsl$",
|
||||
"dest": "~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git": "https://gist.github.com/igv/a015fc885d5c22e6891820ad89555637",
|
||||
"whitelist": "%.glsl$",
|
||||
"dest": "~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git": "https://gist.github.com/igv/2364ffa6e81540f29cb7ab4c9bc05b6b",
|
||||
"whitelist": "%.glsl$",
|
||||
"dest": "~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git": "https://gist.github.com/igv/36508af3ffc84410fe39761d6969be10",
|
||||
"whitelist": "%.glsl$",
|
||||
"dest": "~~/shaders/igv"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/Artoriuz/glsl-joint-bilateral",
|
||||
"branch": "main",
|
||||
"whitelist": "jointbilateral%.glsl$",
|
||||
"dest": "~~/shaders/other"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/Artoriuz/glsl-pixel-clipper",
|
||||
"branch": "main",
|
||||
"whitelist": "%.glsl$",
|
||||
"dest": "~~/shaders/other"
|
||||
},
|
||||
{
|
||||
"git": "https://github.com/bloc97/Anime4K",
|
||||
"whitelist": "%.md$|%.glsl$",
|
||||
"blacklist": "tensorflow",
|
||||
"dest": "~~/shaders/Anime4K"
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
-- https://github.com/Seme4eg/mpv-scripts/blob/master/script-modules/extended-menu.lua
|
||||
|
||||
local mp = require 'mp'
|
||||
local utils = require 'mp.utils'
|
||||
local assdraw = require 'mp.assdraw'
|
||||
@@ -449,7 +451,7 @@ end
|
||||
I was too lazy to list all modifications i've done to the script, but if u
|
||||
rly need to see those - do diff with the original code
|
||||
]]
|
||||
--
|
||||
--
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- START ORIGINAL MPV CODE --
|
||||
@@ -830,58 +832,58 @@ end
|
||||
-- bindings and readline bindings.
|
||||
function em:get_bindings()
|
||||
local bindings = {
|
||||
{ 'ctrl+[', function() self:set_active(false) end },
|
||||
{ 'ctrl+g', function() self:set_active(false) end },
|
||||
{ 'esc', function() self:set_active(false) end },
|
||||
{ 'enter', function() self:handle_enter() end },
|
||||
{ 'kp_enter', function() self:handle_enter() end },
|
||||
{ 'ctrl+m', function() self:handle_enter() end },
|
||||
{ 'bs', function() self:handle_backspace() end },
|
||||
{ 'shift+bs', function() self:handle_backspace() end },
|
||||
{ 'ctrl+h', function() self:handle_backspace() end },
|
||||
{ 'del', function() self:handle_del() end },
|
||||
{ 'shift+del', function() self:handle_del() end },
|
||||
{ 'ins', function() self:handle_ins() end },
|
||||
{ 'shift+ins', function() self:paste(false) end },
|
||||
{ 'mbtn_mid', function() self:paste(false) end },
|
||||
{ 'left', function() self:prev_char() end },
|
||||
{ 'ctrl+b', function() self:prev_char() end },
|
||||
{ 'right', function() self:next_char() end },
|
||||
{ 'ctrl+f', function() self:next_char() end },
|
||||
{ 'ctrl+k', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+p', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+j', function() self:change_selected_index(1) end },
|
||||
{ 'ctrl+n', function() self:change_selected_index(1) end },
|
||||
{ 'up', function() self:move_history(-1) end },
|
||||
{ 'alt+p', function() self:move_history(-1) end },
|
||||
{ 'wheel_up', function() self:move_history(-1) end },
|
||||
{ 'down', function() self:move_history(1) end },
|
||||
{ 'alt+n', function() self:move_history(1) end },
|
||||
{ 'wheel_down', function() self:move_history(1) end },
|
||||
{ 'wheel_left', function() end },
|
||||
{ 'wheel_right', function() end },
|
||||
{ 'ctrl+left', function() self:prev_word() end },
|
||||
{ 'alt+b', function() self:prev_word() end },
|
||||
{ 'ctrl+right', function() self:next_word() end },
|
||||
{ 'alt+f', function() self:next_word() end },
|
||||
{ 'ctrl+a', function() self:go_home() end },
|
||||
{ 'home', function() self:go_home() end },
|
||||
{ 'ctrl+e', function() self:go_end() end },
|
||||
{ 'end', function() self:go_end() end },
|
||||
{ 'ctrl+shift+f',function() self:handle_pgdown() end },
|
||||
{ 'ctrl+shift+b',function() self:handle_pgup() end },
|
||||
{ 'pgdwn', function() self:handle_pgdown() end },
|
||||
{ 'pgup', function() self:handle_pgup() end },
|
||||
{ 'ctrl+c', function() self:clear() end },
|
||||
{ 'ctrl+d', function() self:handle_del() end },
|
||||
{ 'ctrl+u', function() self:del_to_start() end },
|
||||
{ 'ctrl+v', function() self:paste(true) end },
|
||||
{ 'meta+v', function() self:paste(true) end },
|
||||
{ 'ctrl+bs', function() self:del_word() end },
|
||||
{ 'ctrl+w', function() self:del_word() end },
|
||||
{ 'ctrl+del', function() self:del_next_word() end },
|
||||
{ 'alt+d', function() self:del_next_word() end },
|
||||
{ 'kp_dec', function() self:handle_char_input('.') end },
|
||||
{ 'ctrl+[', function() self:set_active(false) end },
|
||||
{ 'ctrl+g', function() self:set_active(false) end },
|
||||
{ 'esc', function() self:set_active(false) end },
|
||||
{ 'enter', function() self:handle_enter() end },
|
||||
{ 'kp_enter', function() self:handle_enter() end },
|
||||
{ 'ctrl+m', function() self:handle_enter() end },
|
||||
{ 'bs', function() self:handle_backspace() end },
|
||||
{ 'shift+bs', function() self:handle_backspace() end },
|
||||
{ 'ctrl+h', function() self:handle_backspace() end },
|
||||
{ 'del', function() self:handle_del() end },
|
||||
{ 'shift+del', function() self:handle_del() end },
|
||||
{ 'ins', function() self:handle_ins() end },
|
||||
{ 'shift+ins', function() self:paste(false) end },
|
||||
{ 'mbtn_mid', function() self:paste(false) end },
|
||||
{ 'left', function() self:prev_char() end },
|
||||
{ 'ctrl+b', function() self:prev_char() end },
|
||||
{ 'right', function() self:next_char() end },
|
||||
{ 'ctrl+f', function() self:next_char() end },
|
||||
{ 'ctrl+k', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+p', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+j', function() self:change_selected_index(1) end },
|
||||
{ 'ctrl+n', function() self:change_selected_index(1) end },
|
||||
{ 'up', function() self:move_history(-1) end },
|
||||
{ 'alt+p', function() self:move_history(-1) end },
|
||||
{ 'wheel_up', function() self:move_history(-1) end },
|
||||
{ 'down', function() self:move_history(1) end },
|
||||
{ 'alt+n', function() self:move_history(1) end },
|
||||
{ 'wheel_down', function() self:move_history(1) end },
|
||||
{ 'wheel_left', function() end },
|
||||
{ 'wheel_right', function() end },
|
||||
{ 'ctrl+left', function() self:prev_word() end },
|
||||
{ 'alt+b', function() self:prev_word() end },
|
||||
{ 'ctrl+right', function() self:next_word() end },
|
||||
{ 'alt+f', function() self:next_word() end },
|
||||
{ 'ctrl+a', function() self:go_home() end },
|
||||
{ 'home', function() self:go_home() end },
|
||||
{ 'ctrl+e', function() self:go_end() end },
|
||||
{ 'end', function() self:go_end() end },
|
||||
{ 'ctrl+shift+f', function() self:handle_pgdown() end },
|
||||
{ 'ctrl+shift+b', function() self:handle_pgup() end },
|
||||
{ 'pgdwn', function() self:handle_pgdown() end },
|
||||
{ 'pgup', function() self:handle_pgup() end },
|
||||
{ 'ctrl+c', function() self:clear() end },
|
||||
{ 'ctrl+d', function() self:handle_del() end },
|
||||
{ 'ctrl+u', function() self:del_to_start() end },
|
||||
{ 'ctrl+v', function() self:paste(true) end },
|
||||
{ 'meta+v', function() self:paste(true) end },
|
||||
{ 'ctrl+bs', function() self:del_word() end },
|
||||
{ 'ctrl+w', function() self:del_word() end },
|
||||
{ 'ctrl+del', function() self:del_next_word() end },
|
||||
{ 'alt+d', function() self:del_next_word() end },
|
||||
{ 'kp_dec', function() self:handle_char_input('.') end },
|
||||
}
|
||||
|
||||
for i = 0, 9 do
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which adds a Favourites path that can be loaded from the ROOT
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
|
||||
local fb = require 'file-browser'
|
||||
local save_path = mp.command_native({"expand-path", "~~/script-opts/file_browser_favourites.txt"}) --[[@as string]]
|
||||
do
|
||||
local file = io.open(save_path, "a+")
|
||||
if not file then
|
||||
msg.error("cannot access file", ("%q"):format(save_path), "make sure that the directory exists")
|
||||
return {}
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
---@type Item[]
|
||||
local favourites = {}
|
||||
local favourites_loaded = false
|
||||
|
||||
---@type ParserConfig
|
||||
local favs = {
|
||||
api_version = "1.8.0",
|
||||
priority = 30,
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
local use_virtual_directory = true
|
||||
|
||||
---@type table<string,string>
|
||||
local full_paths = {}
|
||||
|
||||
---@param str string
|
||||
---@return Item
|
||||
local function create_favourite_object(str)
|
||||
local item = {
|
||||
type = str:sub(-1) == "/" and "dir" or "file",
|
||||
path = str,
|
||||
redirect = not use_virtual_directory,
|
||||
name = str:match("([^/]+/?)$")
|
||||
}
|
||||
full_paths[str:match("([^/]+)/?$")] = str
|
||||
return item
|
||||
end
|
||||
|
||||
---@param self Parser
|
||||
function favs:setup()
|
||||
self:register_root_item('Favourites/')
|
||||
end
|
||||
|
||||
local function update_favourites()
|
||||
local file = io.open(save_path, "r")
|
||||
if not file then return end
|
||||
|
||||
favourites = {}
|
||||
for str in file:lines() do
|
||||
table.insert(favourites, create_favourite_object(str))
|
||||
end
|
||||
file:close()
|
||||
favourites_loaded = true
|
||||
end
|
||||
|
||||
function favs:can_parse(directory)
|
||||
return directory:find("Favourites/") == 1
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param self Parser
|
||||
---@param directory string
|
||||
---@return List?
|
||||
---@return Opts?
|
||||
function favs:parse(directory)
|
||||
if not favourites_loaded then update_favourites() end
|
||||
if directory == "Favourites/" then
|
||||
local opts = {
|
||||
filtered = true,
|
||||
sorted = true
|
||||
}
|
||||
return favourites, opts
|
||||
end
|
||||
|
||||
if use_virtual_directory then
|
||||
-- converts the relative favourite path into a full path
|
||||
local name = directory:match("Favourites/([^/]+)/?")
|
||||
|
||||
local _, finish = directory:find("Favourites/([^/]+/?)")
|
||||
local full_path = (full_paths[name] or "")..directory:sub(finish+1)
|
||||
local list, opts = self:defer(full_path or "")
|
||||
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.id = self:get_id()
|
||||
if opts.directory_label then
|
||||
opts.directory_label = opts.directory_label:gsub(full_paths[name], "Favourites/"..name..'/')
|
||||
if opts.directory_label:find("Favourites/") ~= 1 then opts.directory_label = nil end
|
||||
end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not item.path then item.redirect = false end
|
||||
item.path = item.path or full_path..item.name
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
local path = full_paths[ directory:match("([^/]+/?)$") or "" ]
|
||||
|
||||
local list, opts = self:defer(path)
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.directory = opts.directory or path
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return integer?
|
||||
---@return Item?
|
||||
local function get_favourite(path)
|
||||
for index, value in ipairs(favourites) do
|
||||
if value.path == path then return index, value end
|
||||
end
|
||||
end
|
||||
|
||||
--update the browser with new contents of the file
|
||||
---@async
|
||||
local function update_browser()
|
||||
if favs.get_directory():find("^[fF]avourites/$") then
|
||||
local cursor = favs.get_selected_index()
|
||||
fb.rescan_await()
|
||||
fb.set_selected_index(cursor)
|
||||
else
|
||||
fb.clear_cache({'favourites/', 'Favourites/'})
|
||||
end
|
||||
end
|
||||
|
||||
--write the contents of favourites to the file
|
||||
local function write_to_file()
|
||||
local file = io.open(save_path, "w+")
|
||||
if not file then return msg.error(file, "could not open favourites file") end
|
||||
for _, item in ipairs(favourites) do
|
||||
file:write(string.format("%s\n", item.path))
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function add_favourite(path)
|
||||
if get_favourite(path) then return end
|
||||
update_favourites()
|
||||
table.insert(favourites, create_favourite_object(path))
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function remove_favourite(path)
|
||||
update_favourites()
|
||||
local index = get_favourite(path)
|
||||
if not index then return end
|
||||
table.remove(favourites, index)
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function move_favourite(path, direction)
|
||||
update_favourites()
|
||||
local index, item = get_favourite(path)
|
||||
if not index or not favourites[index + direction] then return end
|
||||
|
||||
favourites[index] = favourites[index + direction]
|
||||
favourites[index + direction] = item
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function toggle_favourite(cmd, state, co)
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
if state.directory:find("[fF]avourites/$") then remove_favourite(path)
|
||||
else add_favourite(path) end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function move_key(cmd, state, co)
|
||||
if not state.directory:find("[fF]avourites/") then return false end
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
local cursor = fb.get_selected_index()
|
||||
if cmd.name == favs:get_id().."/move_up" then
|
||||
move_favourite(path, -1)
|
||||
fb.set_selected_index(cursor-1)
|
||||
else
|
||||
move_favourite(path, 1)
|
||||
fb.set_selected_index(cursor+1)
|
||||
end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
update_favourites()
|
||||
|
||||
favs.keybinds = {
|
||||
{ "F", "toggle_favourite", toggle_favourite, {}, },
|
||||
{ "Ctrl+UP", "move_up", move_key, {repeatable = true} },
|
||||
{ "Ctrl+DOWN", "move_down", move_key, {repeatable = true} },
|
||||
}
|
||||
|
||||
return favs
|
||||
@@ -1,39 +0,0 @@
|
||||
--[[
|
||||
An addon for file-browser which decodes URLs so that they are more readable
|
||||
]]
|
||||
|
||||
---@type ParserConfig
|
||||
local urldecode = {
|
||||
priority = 5,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
--decodes a URL address
|
||||
--this piece of code was taken from: https://stackoverflow.com/questions/20405985/lua-decodeuri-luvit/20406960#20406960
|
||||
---@type fun(s: string): string
|
||||
local decodeURI
|
||||
do
|
||||
local char, gsub, tonumber = string.char, string.gsub, tonumber
|
||||
local function _(hex) return char(tonumber(hex, 16)) end
|
||||
|
||||
function decodeURI(s)
|
||||
s = gsub(s, '%%(%x%x)', _)
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
function urldecode:can_parse(directory)
|
||||
return self.get_protocol(directory) ~= nil
|
||||
end
|
||||
|
||||
---@async
|
||||
function urldecode:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
opts = opts or {}
|
||||
if opts.directory and not self.get_protocol(opts.directory) then return list, opts end
|
||||
|
||||
opts.directory_label = decodeURI(opts.directory_label or (opts.directory or directory))
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return urldecode
|
||||
@@ -1,206 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which adds a Favourites path that can be loaded from the ROOT
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
|
||||
local fb = require 'file-browser'
|
||||
local save_path = mp.command_native({"expand-path", "~~/script-opts/file_browser_favourites.txt"}) --[[@as string]]
|
||||
do
|
||||
local file = io.open(save_path, "a+")
|
||||
if not file then
|
||||
msg.error("cannot access file", ("%q"):format(save_path), "make sure that the directory exists")
|
||||
return {}
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
---@type Item[]
|
||||
local favourites = {}
|
||||
local favourites_loaded = false
|
||||
|
||||
---@type ParserConfig
|
||||
local favs = {
|
||||
api_version = "1.8.0",
|
||||
priority = 30,
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
local use_virtual_directory = true
|
||||
|
||||
---@type table<string,string>
|
||||
local full_paths = {}
|
||||
|
||||
---@param str string
|
||||
---@return Item
|
||||
local function create_favourite_object(str)
|
||||
local item = {
|
||||
type = str:sub(-1) == "/" and "dir" or "file",
|
||||
path = str,
|
||||
redirect = not use_virtual_directory,
|
||||
name = str:match("([^/]+/?)$")
|
||||
}
|
||||
full_paths[str:match("([^/]+)/?$")] = str
|
||||
return item
|
||||
end
|
||||
|
||||
---@param self Parser
|
||||
function favs:setup()
|
||||
self:register_root_item('Favourites/')
|
||||
end
|
||||
|
||||
local function update_favourites()
|
||||
local file = io.open(save_path, "r")
|
||||
if not file then return end
|
||||
|
||||
favourites = {}
|
||||
for str in file:lines() do
|
||||
table.insert(favourites, create_favourite_object(str))
|
||||
end
|
||||
file:close()
|
||||
favourites_loaded = true
|
||||
end
|
||||
|
||||
function favs:can_parse(directory)
|
||||
return directory:find("Favourites/") == 1
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param self Parser
|
||||
---@param directory string
|
||||
---@return List?
|
||||
---@return Opts?
|
||||
function favs:parse(directory)
|
||||
if not favourites_loaded then update_favourites() end
|
||||
if directory == "Favourites/" then
|
||||
local opts = {
|
||||
filtered = true,
|
||||
sorted = true
|
||||
}
|
||||
return favourites, opts
|
||||
end
|
||||
|
||||
if use_virtual_directory then
|
||||
-- converts the relative favourite path into a full path
|
||||
local name = directory:match("Favourites/([^/]+)/?")
|
||||
|
||||
local _, finish = directory:find("Favourites/([^/]+/?)")
|
||||
local full_path = (full_paths[name] or "")..directory:sub(finish+1)
|
||||
local list, opts = self:defer(full_path or "")
|
||||
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.id = self:get_id()
|
||||
if opts.directory_label then
|
||||
opts.directory_label = opts.directory_label:gsub(full_paths[name], "Favourites/"..name..'/')
|
||||
if opts.directory_label:find("Favourites/") ~= 1 then opts.directory_label = nil end
|
||||
end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not item.path then item.redirect = false end
|
||||
item.path = item.path or full_path..item.name
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
local path = full_paths[ directory:match("([^/]+/?)$") or "" ]
|
||||
|
||||
local list, opts = self:defer(path)
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.directory = opts.directory or path
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return integer?
|
||||
---@return Item?
|
||||
local function get_favourite(path)
|
||||
for index, value in ipairs(favourites) do
|
||||
if value.path == path then return index, value end
|
||||
end
|
||||
end
|
||||
|
||||
--update the browser with new contents of the file
|
||||
---@async
|
||||
local function update_browser()
|
||||
if favs.get_directory():find("^[fF]avourites/$") then
|
||||
local cursor = favs.get_selected_index()
|
||||
fb.rescan_await()
|
||||
fb.set_selected_index(cursor)
|
||||
else
|
||||
fb.clear_cache({'favourites/', 'Favourites/'})
|
||||
end
|
||||
end
|
||||
|
||||
--write the contents of favourites to the file
|
||||
local function write_to_file()
|
||||
local file = io.open(save_path, "w+")
|
||||
if not file then return msg.error(file, "could not open favourites file") end
|
||||
for _, item in ipairs(favourites) do
|
||||
file:write(string.format("%s\n", item.path))
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function add_favourite(path)
|
||||
if get_favourite(path) then return end
|
||||
update_favourites()
|
||||
table.insert(favourites, create_favourite_object(path))
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function remove_favourite(path)
|
||||
update_favourites()
|
||||
local index = get_favourite(path)
|
||||
if not index then return end
|
||||
table.remove(favourites, index)
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function move_favourite(path, direction)
|
||||
update_favourites()
|
||||
local index, item = get_favourite(path)
|
||||
if not index or not favourites[index + direction] then return end
|
||||
|
||||
favourites[index] = favourites[index + direction]
|
||||
favourites[index + direction] = item
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function toggle_favourite(cmd, state, co)
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
if state.directory:find("[fF]avourites/$") then remove_favourite(path)
|
||||
else add_favourite(path) end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function move_key(cmd, state, co)
|
||||
if not state.directory:find("[fF]avourites/") then return false end
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
local cursor = fb.get_selected_index()
|
||||
if cmd.name == favs:get_id().."/move_up" then
|
||||
move_favourite(path, -1)
|
||||
fb.set_selected_index(cursor-1)
|
||||
else
|
||||
move_favourite(path, 1)
|
||||
fb.set_selected_index(cursor+1)
|
||||
end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
update_favourites()
|
||||
|
||||
favs.keybinds = {
|
||||
{ "F", "toggle_favourite", toggle_favourite, {}, },
|
||||
{ "Ctrl+UP", "move_up", move_key, {repeatable = true} },
|
||||
{ "Ctrl+DOWN", "move_down", move_key, {repeatable = true} },
|
||||
}
|
||||
|
||||
return favs
|
||||
@@ -1,45 +0,0 @@
|
||||
local fb = require "file-browser"
|
||||
local opt = require "mp.options"
|
||||
|
||||
local o = {
|
||||
--list of absolute paths separated by the root separators
|
||||
paths = ""
|
||||
}
|
||||
|
||||
--config file stored in ~~/script-opts/file-browser/filter.conf
|
||||
opt.read_options(o, "file-browser/filter")
|
||||
|
||||
local parser = {
|
||||
priority = 10,
|
||||
api_version = "1.3.0"
|
||||
}
|
||||
|
||||
local paths = {}
|
||||
for str in fb.iterate_opt(o.paths) do
|
||||
paths[str] = true
|
||||
end
|
||||
|
||||
local function filter(path)
|
||||
return paths[path]
|
||||
end
|
||||
|
||||
function parser:can_parse()
|
||||
return true
|
||||
end
|
||||
|
||||
function parser:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
if not list then return list, opts end
|
||||
|
||||
directory = opts.directory or directory
|
||||
|
||||
for i=#list, 1, -1 do
|
||||
if filter( fb.get_full_path(list[i], directory) ) then
|
||||
table.remove(list, i)
|
||||
end
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return parser
|
||||
@@ -1,41 +0,0 @@
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local isos = {
|
||||
name = 'iso-loader',
|
||||
priority = 20,
|
||||
api_version = '1.5'
|
||||
}
|
||||
|
||||
function isos:setup()
|
||||
fb.add_default_extension('iso')
|
||||
end
|
||||
|
||||
function isos:can_parse()
|
||||
return true
|
||||
end
|
||||
|
||||
function isos:parse(directory, parse_state)
|
||||
local list, opts = self:defer(directory, parse_state)
|
||||
if not list or #list == 0 then return list, opts end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
local path = fb.get_full_path(item, opts.directory or directory)
|
||||
if fb.get_extension(path) == 'iso' then
|
||||
item.mpv_options = { ['bluray-device'] = path, ['dvd-device'] = path }
|
||||
item.path = 'bd://'
|
||||
end
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
mp.add_hook('on_load_fail', 50, function()
|
||||
if mp.get_property('stream-open-filename') == 'bd://' then
|
||||
msg.info('failed to load bluray-device, attempting dvd-device')
|
||||
mp.set_property('stream-open-filename', 'dvd://')
|
||||
end
|
||||
end)
|
||||
|
||||
return isos
|
||||
@@ -1,124 +0,0 @@
|
||||
--[[
|
||||
This file is an internal file-browser addon.
|
||||
It should not be imported like a normal module.
|
||||
|
||||
Allows searching the current directory.
|
||||
]]--
|
||||
|
||||
local msg = require "mp.msg"
|
||||
local fb = require "file-browser"
|
||||
local input_loaded, input = pcall(require, "mp.input")
|
||||
local user_input_loaded, user_input = pcall(require, "user-input-module")
|
||||
|
||||
---@type ParserConfig
|
||||
local find = {
|
||||
api_version = "1.3.0"
|
||||
}
|
||||
|
||||
---@type thread|nil
|
||||
local latest_coroutine = nil
|
||||
|
||||
---@type State
|
||||
local global_fb_state = getmetatable(fb.get_state()).__original
|
||||
|
||||
---@param name string
|
||||
---@param query string
|
||||
---@return boolean
|
||||
local function compare(name, query)
|
||||
if name:find(query) then return true end
|
||||
if name:lower():find(query) then return true end
|
||||
if name:upper():find(query) then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param key Keybind
|
||||
---@param state State
|
||||
---@param co thread
|
||||
---@return boolean?
|
||||
local function main(key, state, co)
|
||||
if not state.list then return false end
|
||||
|
||||
---@type string
|
||||
local text
|
||||
if key.name == "find/find" then text = "Find: enter search string"
|
||||
else text = "Find: enter advanced search string" end
|
||||
|
||||
if input_loaded then
|
||||
input.get({
|
||||
prompt = text .. "\n>",
|
||||
id = "file-browser/find",
|
||||
submit = fb.coroutine.callback(),
|
||||
})
|
||||
elseif user_input_loaded then
|
||||
user_input.get_user_input( fb.coroutine.callback(), { text = text, id = "find", replace = true } )
|
||||
end
|
||||
|
||||
local query, error = coroutine.yield()
|
||||
if input_loaded then input.terminate() end
|
||||
if not query then return msg.debug(error) end
|
||||
|
||||
-- allow the directory to be changed before this point
|
||||
local list = fb.get_list()
|
||||
local parse_id = global_fb_state.co
|
||||
|
||||
if key.name == "find/find" then
|
||||
query = fb.pattern_escape(query)
|
||||
end
|
||||
|
||||
local results = {}
|
||||
|
||||
for index, item in ipairs(list) do
|
||||
if compare(item.label or item.name, query) then
|
||||
table.insert(results, index)
|
||||
end
|
||||
end
|
||||
|
||||
if (#results < 1) then
|
||||
msg.warn("No matching items for '"..query.."'")
|
||||
return
|
||||
end
|
||||
|
||||
--keep cycling through the search results if any are found
|
||||
--putting this into a separate coroutine removes any passthrough ambiguity
|
||||
--the final return statement should return to `step_find` not any other function
|
||||
---@async
|
||||
fb.coroutine.run(function()
|
||||
latest_coroutine = coroutine.running()
|
||||
---@type number
|
||||
local rindex = 1
|
||||
while (true) do
|
||||
|
||||
if rindex == 0 then rindex = #results
|
||||
elseif rindex == #results + 1 then rindex = 1 end
|
||||
|
||||
fb.set_selected_index(results[rindex])
|
||||
local direction = coroutine.yield(true) --[[@as number]]
|
||||
rindex = rindex + direction
|
||||
|
||||
if parse_id ~= global_fb_state.co then
|
||||
latest_coroutine = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function step_find(key)
|
||||
if not latest_coroutine then return false end
|
||||
---@type number
|
||||
local direction = 0
|
||||
if key.name == "find/next" then direction = 1
|
||||
elseif key.name == "find/prev" then direction = -1 end
|
||||
return fb.coroutine.resume_err(latest_coroutine, direction)
|
||||
end
|
||||
|
||||
find.keybinds = {
|
||||
{"Ctrl+f", "find", main, {}},
|
||||
{"Ctrl+F", "find_advanced", main, {}},
|
||||
{"n", "next", step_find, {}},
|
||||
{"N", "prev", step_find, {}},
|
||||
}
|
||||
|
||||
return find
|
||||
@@ -1,31 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which displays ~/ for the home directory instead of the full path
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local fb = require "file-browser"
|
||||
|
||||
local home = fb.fix_path(mp.command_native({"expand-path", "~/"}) --[[@as string]], true)
|
||||
|
||||
---@type ParserConfig
|
||||
local home_label = {
|
||||
priority = 100,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
function home_label:can_parse(directory)
|
||||
if not fb.get_opt('home_label') then return false end
|
||||
return directory:sub(1, home:len()) == home
|
||||
end
|
||||
|
||||
---@async
|
||||
function home_label:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
if not opts then opts = {} end
|
||||
if (not opts.directory or opts.directory == directory) and not opts.directory_label then
|
||||
opts.directory_label = "~/"..(directory:sub(home:len()+1) or "")
|
||||
end
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return home_label
|
||||
@@ -1,218 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which uses the Windows dir command to parse native directories
|
||||
This behaves near identically to the native parser, but IO is done asynchronously.
|
||||
|
||||
Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
local fb = require "file-browser"
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
---@param bytes string
|
||||
---@return fun(): number, number
|
||||
local function byte_iterator(bytes)
|
||||
---@async
|
||||
---@return number?
|
||||
local function iter()
|
||||
for i = 1, #bytes do
|
||||
coroutine.yield(bytes:byte(i), i)
|
||||
end
|
||||
error('malformed utf16le string - expected byte but found end of string')
|
||||
end
|
||||
|
||||
return coroutine.wrap(iter)
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param by number
|
||||
---@return number
|
||||
local function lshift(bits, by)
|
||||
return bits * 2^by
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param by number
|
||||
---@return integer
|
||||
local function rshift(bits, by)
|
||||
return math.floor(bits / 2^by)
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param i number
|
||||
---@return number
|
||||
local function bits_below(bits, i)
|
||||
return bits % 2^i
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param i number exclusive
|
||||
---@param j number inclusive
|
||||
---@return integer
|
||||
local function bits_between(bits, i, j)
|
||||
return rshift(bits_below(bits, j), i)
|
||||
end
|
||||
|
||||
---@param bytes string
|
||||
---@return number[]
|
||||
local function utf16le_to_unicode(bytes)
|
||||
msg.trace('converting from utf16-le to unicode codepoints')
|
||||
|
||||
---@type number[]
|
||||
local codepoints = {}
|
||||
|
||||
local get_byte = byte_iterator(bytes)
|
||||
|
||||
while true do
|
||||
-- start of a char
|
||||
local success, little, i = pcall(get_byte)
|
||||
if not success then break end
|
||||
|
||||
local big = get_byte()
|
||||
local codepoint = little + lshift(big, 8)
|
||||
|
||||
if codepoint < 0xd800 or codepoint > 0xdfff then
|
||||
table.insert(codepoints, codepoint)
|
||||
else
|
||||
-- handling surrogate pairs
|
||||
-- grab the next two bytes to grab the low surrogate
|
||||
local high_pair = codepoint
|
||||
local low_pair = get_byte() + lshift(get_byte(), 8)
|
||||
|
||||
if high_pair >= 0xdc00 then
|
||||
error(('malformed utf16le string at byte #%d (0x%04X) - high surrogate pair should be < 0xDC00'):format(i, high_pair))
|
||||
elseif low_pair < 0xdc00 then
|
||||
error(('malformed utf16le string at byte #%d (0x%04X) - low surrogate pair should be >= 0xDC00'):format(i+2, low_pair))
|
||||
end
|
||||
|
||||
-- The last 10 bits of each surrogate are the two halves of the codepoint
|
||||
-- https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
|
||||
local high_bits = bits_below(high_pair, 10)
|
||||
local low_bits = bits_below(low_pair, 10)
|
||||
local surrogate_par = (low_bits + lshift(high_bits, 10)) + 0x10000
|
||||
|
||||
table.insert(codepoints, surrogate_par)
|
||||
end
|
||||
end
|
||||
|
||||
return codepoints
|
||||
end
|
||||
|
||||
---@param codepoints number[]
|
||||
---@return string
|
||||
local function unicode_to_utf8(codepoints)
|
||||
---@type number[]
|
||||
local bytes = {}
|
||||
|
||||
-- https://en.wikipedia.org/wiki/UTF-8#Description
|
||||
for i, codepoint in ipairs(codepoints) do
|
||||
if codepoint >= 0xd800 and codepoint <= 0xdfff then
|
||||
error(('codepoint %d (U+%05X) is within the reserved surrogate pair range (U+D800-U+DFFF)'):format(i, codepoint))
|
||||
elseif codepoint <= 0x7f then
|
||||
table.insert(bytes, codepoint)
|
||||
elseif codepoint <= 0x7ff then
|
||||
table.insert(bytes, 0xC0 + rshift(codepoint, 6))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
elseif codepoint <= 0xffff then
|
||||
table.insert(bytes, 0xE0 + rshift(codepoint, 12))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
elseif codepoint <= 0x10ffff then
|
||||
table.insert(bytes, 0xF0 + rshift(codepoint, 18))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 12, 18))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
else
|
||||
error(('codepoint %d (U+%05X) is larger than U+10FFFF'):format(i, codepoint))
|
||||
end
|
||||
end
|
||||
|
||||
return string.char(table.unpack(bytes))
|
||||
end
|
||||
|
||||
local function utf8(text)
|
||||
return unicode_to_utf8(utf16le_to_unicode(text))
|
||||
end
|
||||
|
||||
---@type ParserConfig
|
||||
local dir = {
|
||||
priority = 109,
|
||||
api_version = "1.9.0",
|
||||
name = "cmd-dir",
|
||||
keybind_name = "file"
|
||||
}
|
||||
|
||||
---@async
|
||||
---@param args string[]
|
||||
---@param parse_state ParseState
|
||||
---@return string|nil
|
||||
local function command(args, parse_state)
|
||||
local async = mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
args = args,
|
||||
}, fb.coroutine.callback(30) )
|
||||
|
||||
---@type boolean, boolean, MPVSubprocessResult
|
||||
local completed, _, cmd = parse_state:yield()
|
||||
if not completed then
|
||||
msg.warn('read timed out for:', table.unpack(args))
|
||||
mp.abort_async_command(async)
|
||||
return nil
|
||||
end
|
||||
|
||||
local success = xpcall(function()
|
||||
cmd.stdout = utf8(cmd.stdout) or ''
|
||||
cmd.stderr = utf8(cmd.stderr) or ''
|
||||
end, fb.traceback)
|
||||
|
||||
if not success then return msg.error('failed to convert utf16-le string to utf8') end
|
||||
|
||||
--dir returns this exact error message if the directory is empty
|
||||
if cmd.status == 1 and cmd.stderr == "File Not Found\r\n" then cmd.status = 0 end
|
||||
if cmd.status ~= 0 then return msg.error(cmd.stderr) end
|
||||
|
||||
return cmd.status == 0 and cmd.stdout or nil
|
||||
end
|
||||
|
||||
function dir:can_parse(directory)
|
||||
if not fb.get_opt('windir_parser') then return false end
|
||||
return PLATFORM == 'windows' and directory ~= '' and not fb.get_protocol(directory)
|
||||
end
|
||||
|
||||
---@async
|
||||
function dir:parse(directory, parse_state)
|
||||
local list = {}
|
||||
|
||||
-- the dir command expects backslashes for our paths
|
||||
directory = string.gsub(directory, "/", "\\")
|
||||
|
||||
local dirs = command({ "cmd", "/U", "/c", "dir", "/b", "/ad", directory }, parse_state)
|
||||
if not dirs then return end
|
||||
|
||||
local files = command({ "cmd", "/U", "/c", "dir", "/b", "/a-d", directory }, parse_state)
|
||||
if not files then return end
|
||||
|
||||
for name in dirs:gmatch("[^\n\r]+") do
|
||||
name = name.."/"
|
||||
if fb.valid_dir(name) then
|
||||
table.insert(list, { name = name, type = "dir" })
|
||||
msg.trace(name)
|
||||
end
|
||||
end
|
||||
|
||||
for name in files:gmatch("[^\n\r]+") do
|
||||
if fb.valid_file(name) then
|
||||
table.insert(list, { name = name, type = "file" })
|
||||
msg.trace(name)
|
||||
end
|
||||
end
|
||||
|
||||
return list, { filtered = true }
|
||||
end
|
||||
|
||||
return dir
|
||||
@@ -1,62 +0,0 @@
|
||||
--[[
|
||||
This file is an internal file-browser addon.
|
||||
It should not be imported like a normal module.
|
||||
|
||||
Automatically populates the root with windows drives on startup.
|
||||
Ctrl+r will add new drives mounted since startup.
|
||||
|
||||
Drives will only be added if they are not already present in the root.
|
||||
]]
|
||||
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
---returns a list of windows drives
|
||||
---@return string[]?
|
||||
local function get_drives()
|
||||
---@type MPVSubprocessResult?, string?
|
||||
local result, err = mp.command_native({
|
||||
name = 'subprocess',
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
args = {'fsutil', 'fsinfo', 'drives'}
|
||||
})
|
||||
if not result then return msg.error(err) end
|
||||
if result.status ~= 0 then return msg.error('could not read windows root') end
|
||||
|
||||
local root = {}
|
||||
for drive in result.stdout:gmatch("(%a:)\\") do
|
||||
table.insert(root, drive..'/')
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
-- adds windows drives to the root if they are not already present
|
||||
local function import_drives()
|
||||
if fb.get_opt('auto_detect_windows_drives') and PLATFORM ~= 'windows' then return end
|
||||
|
||||
local drives = get_drives()
|
||||
if not drives then return end
|
||||
|
||||
for _, drive in ipairs(drives) do
|
||||
fb.register_root_item(drive)
|
||||
end
|
||||
end
|
||||
|
||||
local keybind = {
|
||||
key = 'Ctrl+r',
|
||||
name = 'import_root_drives',
|
||||
command = import_drives,
|
||||
parser = 'root',
|
||||
passthrough = true
|
||||
}
|
||||
|
||||
---@type ParserConfig
|
||||
return {
|
||||
api_version = '1.9.0',
|
||||
setup = import_drives,
|
||||
keybinds = { keybind }
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local parser = {
|
||||
priority = 105,
|
||||
api_version = '1.2.0'
|
||||
}
|
||||
|
||||
-- stores a table of the parsers loaded by file-browser
|
||||
-- we will use this to check if a parser is for a local file system
|
||||
local parsers
|
||||
|
||||
local sort_mode = 0
|
||||
|
||||
function parser:setup()
|
||||
parsers = fb.get_parsers()
|
||||
end
|
||||
|
||||
function parser:parse(directory)
|
||||
if sort_mode == 0 or fb.get_protocol(directory) then return end
|
||||
local list, opts = self:defer(directory)
|
||||
if not list then return list, opts end
|
||||
|
||||
-- Only run this on parsers that are for the local filesystem.
|
||||
-- We assume that custom addons for the local filesystem are setting the keybind_name field to 'file'
|
||||
-- for compatability.
|
||||
if parsers[opts.id] then
|
||||
if parsers[opts.id].keybind_name ~= 'file' and parsers[opts.id].name ~= 'file' then
|
||||
return list, opts
|
||||
end
|
||||
end
|
||||
|
||||
directory = opts.directory or directory
|
||||
local cache = {}
|
||||
|
||||
-- gets the file info of an item
|
||||
-- uses memoisation to speed things up
|
||||
function get_file_info(item)
|
||||
if cache[item] then return cache[item] end
|
||||
|
||||
local path = fb.get_full_path(item, directory)
|
||||
local file_info = utils.file_info(path)
|
||||
if not file_info then
|
||||
msg.warn('failed to read file info for', path)
|
||||
return {}
|
||||
end
|
||||
|
||||
cache[item] = file_info
|
||||
return file_info
|
||||
end
|
||||
|
||||
-- sorts the items based on the latest modification time
|
||||
-- if mtime is undefined due to a file read failure then use 0
|
||||
table.sort(list, function(a, b)
|
||||
-- `dir` will compare as less than `file`
|
||||
if a.type ~= b.type then return a.type < b.type end
|
||||
if sort_mode == 1 then
|
||||
return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0)
|
||||
elseif sort_mode == 2 then
|
||||
return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0)
|
||||
elseif sort_mode == 3 then
|
||||
return (get_file_info(a).size or 0) < (get_file_info(b).size or 0)
|
||||
elseif sort_mode == 4 then
|
||||
return (get_file_info(a).size or 0) > (get_file_info(b).size or 0)
|
||||
end
|
||||
end)
|
||||
|
||||
opts.sorted = true
|
||||
return list, opts
|
||||
end
|
||||
|
||||
-- adds the keybind to toggle sorting
|
||||
parser.keybinds = {
|
||||
{
|
||||
key = '^',
|
||||
name = 'toggle_sort',
|
||||
command = function()
|
||||
sort_mode = sort_mode + 1
|
||||
if sort_mode > 4 then sort_mode = 0 end
|
||||
fb.rescan()
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
return parser
|
||||
@@ -1,39 +0,0 @@
|
||||
--[[
|
||||
An addon for file-browser which decodes URLs so that they are more readable
|
||||
]]
|
||||
|
||||
---@type ParserConfig
|
||||
local urldecode = {
|
||||
priority = 5,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
--decodes a URL address
|
||||
--this piece of code was taken from: https://stackoverflow.com/questions/20405985/lua-decodeuri-luvit/20406960#20406960
|
||||
---@type fun(s: string): string
|
||||
local decodeURI
|
||||
do
|
||||
local char, gsub, tonumber = string.char, string.gsub, tonumber
|
||||
local function _(hex) return char(tonumber(hex, 16)) end
|
||||
|
||||
function decodeURI(s)
|
||||
s = gsub(s, '%%(%x%x)', _)
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
function urldecode:can_parse(directory)
|
||||
return self.get_protocol(directory) ~= nil
|
||||
end
|
||||
|
||||
---@async
|
||||
function urldecode:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
opts = opts or {}
|
||||
if opts.directory and not self.get_protocol(opts.directory) then return list, opts end
|
||||
|
||||
opts.directory_label = decodeURI(opts.directory_label or (opts.directory or directory))
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return urldecode
|
||||
@@ -1,51 +0,0 @@
|
||||
local fb = require "file-browser"
|
||||
local fb_utils = require 'modules.utils'
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
-- Only enable Windows-specific sorting on Windows platforms
|
||||
if PLATFORM == 'windows' then
|
||||
-- this code is based on https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
|
||||
local ffi = require "ffi"
|
||||
local winapi = {
|
||||
ffi = ffi,
|
||||
C = ffi.C,
|
||||
CP_UTF8 = 65001,
|
||||
shlwapi = ffi.load("shlwapi"),
|
||||
}
|
||||
|
||||
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
|
||||
ffi.cdef[[
|
||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
|
||||
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
|
||||
]]
|
||||
|
||||
winapi.utf8_to_wide = function(utf8_str)
|
||||
if utf8_str then
|
||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
|
||||
|
||||
if utf16_len > 0 then
|
||||
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||
|
||||
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
|
||||
return utf16_str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
fb_utils.sort = function (t)
|
||||
table.sort(t, function(a, b)
|
||||
local a_wide = winapi.utf8_to_wide(a.type:sub(1, 1) .. (a.label or a.name))
|
||||
local b_wide = winapi.utf8_to_wide(b.type:sub(1, 1) .. (b.label or b.name))
|
||||
return winapi.shlwapi.StrCmpLogicalW(a_wide, b_wide) == -1
|
||||
end)
|
||||
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
return { api_version = '1.2.0' }
|
||||
@@ -1,3 +0,0 @@
|
||||
##指定在file-browser文件浏览器中需隐藏的目录,以逗号分隔
|
||||
##示例:F:/$RECYCLE.BIN/
|
||||
paths=
|
||||
@@ -1,238 +0,0 @@
|
||||
#######################################################
|
||||
# This is the default config file for mpv-file-browser
|
||||
# https://github.com/CogentRedTester/mpv-file-browser
|
||||
#######################################################
|
||||
|
||||
# root 目录,以逗号分隔
|
||||
# on linux,你可能想要添加"/",
|
||||
# on windows,这应该用于添加不同的驱动器号
|
||||
# Examples:
|
||||
# linux:
|
||||
# root=~/,/
|
||||
# windows:
|
||||
# root=~/,C:/
|
||||
root=~/
|
||||
|
||||
# characters 单独的根目录,每个字符单独工作
|
||||
# 以防万一个人使用具有奇怪名称的目录
|
||||
root_separators=,
|
||||
|
||||
# 要同时显示在屏幕上的条目数量
|
||||
num_entries=20
|
||||
|
||||
# 要保留历史记录的目录数,大小为 0 时禁用历史记录
|
||||
history_size=100
|
||||
|
||||
# 目录是否循环滚动,默认 yes
|
||||
wrap=yes
|
||||
|
||||
# 是否启用插件,默认:no
|
||||
addons=yes
|
||||
|
||||
# 启用自定义键绑定
|
||||
# he keybind json 文件必须位于 ~~/script-opts
|
||||
custom_keybinds=yes
|
||||
|
||||
# 自动检测 Windows 驱动器并将其添加到根目录
|
||||
# 在根目录下使用 Ctrl+r 会运行另一次扫描
|
||||
auto_detect_windows_drives=yes
|
||||
|
||||
# 当空闲模式下打开浏览器时,首选当前工作目录而不是根目录
|
||||
# 工作目录无论如何都被设置为"当前"目录,因此播放时浏览器将自动定位至当前工作目录,即使此选项设置为 no
|
||||
default_to_working_directory=no
|
||||
|
||||
# 打开浏览器时,更喜欢由文件浏览器的先前 MPV 实例打开的目录
|
||||
# 覆盖`default_to_working_directory`选项
|
||||
# 需要`save_last_opened_directory`为 yes
|
||||
# 使用内部开放的 `last-opened-directory` 插件
|
||||
default_to_last_opened_directory=no
|
||||
|
||||
# 是否保存最后一个打开的目录
|
||||
save_last_opened_directory=no
|
||||
|
||||
# 播放文件更改时,将光标移至当前播放项目(如果有)
|
||||
cursor_follows_playing_item=no
|
||||
|
||||
####################################
|
||||
########## filter settings #########
|
||||
####################################
|
||||
|
||||
# 只在浏览器中显示与 mpv 兼容的文件
|
||||
filter_files=yes
|
||||
|
||||
# file 浏览器仅显示默认情况下与 mpv 兼容的文件
|
||||
# 加入此列表中的文件扩展名将将其添加到扩展名白名单中
|
||||
# 用根分隔符分隔,请勿使用任何空格
|
||||
extension_whitelist=amv;bdmv;ifo;iso
|
||||
|
||||
# 加入此列表的文件扩展名以禁用默认文件类型
|
||||
# 这将覆盖上面以及下面所有的白名单选项
|
||||
#extension_blacklist=mpls
|
||||
|
||||
# 加入此列表中的文件扩展名将会添加到外挂音轨扩展名白名单中
|
||||
# 用根分隔符分隔,请勿使用任何空格
|
||||
audio_extensions=mka,dts,dtshd,dts-hd,truehd,true-hd,flac
|
||||
|
||||
# 加入此列表中的文件扩展名将会添加到字幕扩展名白名单中
|
||||
# 用根分隔符分隔,请勿使用任何空格
|
||||
subtitle_extensions=etf,etf8,utf-8,idx,sub,srt,rt,ssa,ass,mks,vtt,sup,scc,smi,lrc,pgs
|
||||
|
||||
# 过滤 .config 等以 '.' 开头的目录或文件
|
||||
# 用于 linux 系统
|
||||
#filter_dot_dirs=no
|
||||
#filter_dot_files=no
|
||||
|
||||
####################################
|
||||
###### file loading settings #######
|
||||
####################################
|
||||
|
||||
# 这个选项可反转 alt+ENTER 键绑定的行为
|
||||
# 当禁用密钥绑定,则需要为文件启用自动加载
|
||||
# 当启用键绑定将禁用文件的自动加载
|
||||
autoload=no
|
||||
|
||||
# 启用在将项目追加到播放列表时同时递归目录的功能(实验性),默认值:no
|
||||
# 此功能在将插件与异步 IO 结合使用时具有巨大的性能改进潜力
|
||||
concurrent_recursion=yes
|
||||
|
||||
# 可以并发运行的最大递归数量
|
||||
# 如果此数字太高,则可能会使 mpv 事件队列溢出,从而导致某些目录被完全丢弃,默认值:16
|
||||
max_concurrency=16
|
||||
|
||||
# 将本地文件追加到播放列表时,用正斜杠代替反斜杠
|
||||
# 在 Windows 系统上可能有用,默认值:no
|
||||
substitute_backslash=no
|
||||
|
||||
# 如果通过选择当前播放的文件触发自动加载,则当前文件在关闭和重新打开之前将保存其稍后观看的配置
|
||||
# 禁用时当前文件将不会重新启动
|
||||
autoload_save_current=yes
|
||||
|
||||
####################################
|
||||
### directory parsing settings #####
|
||||
####################################
|
||||
|
||||
# 目录缓存用于提高目录读取速度,
|
||||
# 如果加载目录需要较长时间,可以启用此功能。
|
||||
# 但可能会导致显示“幽灵”文件(已删除但仍然存在)
|
||||
# 或者无法显示最近创建的文件。
|
||||
# 使用 Ctrl+r 重新加载目录时不会使用缓存。
|
||||
# 使用 Ctrl+Shift+r 可强制清除缓存。
|
||||
cache=no
|
||||
|
||||
# 启用内部 `ls` 插件,该插件使用 `ls` 命令解析目录。
|
||||
# 允许目录解析并行运行,从而防止浏览器卡顿。
|
||||
# 在 Windows 系统上会自动禁用此功能。
|
||||
ls_parser=yes
|
||||
|
||||
# 启用内部 `windir` 插件,该插件使用 cmd.exe 中的 `dir` 命令解析目录。
|
||||
# 允许目录解析并行运行,从而防止浏览器卡顿。
|
||||
# 在非 Windows 系统上会自动禁用此功能。
|
||||
windir_parser=no
|
||||
|
||||
# 向上移动目录时,不要停止在空协议方案上,例如 `ftp://`
|
||||
# 例如从 `ftp://localhost/` 向上移动将直接移动到根目录,而不是 `ftp://`
|
||||
skip_protocol_schemes=yes
|
||||
|
||||
# 将光盘的驱动路径映射到它们各自的文件路径
|
||||
# 例如,将 bd:// 映射到 bluray-device 属性的值
|
||||
map_bd_device=yes
|
||||
map_dvd_device=yes
|
||||
map_cdda_device=yes
|
||||
|
||||
####################################
|
||||
########## misc settings ###########
|
||||
####################################
|
||||
|
||||
# 是否启用脚本信息来控制空闲屏幕上的徽标文字的显示
|
||||
toggle_idlescreen=no
|
||||
|
||||
# 将路径中的反斜杠 '\' 解释为正斜杠 '/'
|
||||
# 这在 Windows 上很有用,因为 Windows 本身使用反斜杠。
|
||||
# 由于反斜杠是 Unix 系统中有效的文件名字符,因此可能导致路径损坏,但此类文件名很少见
|
||||
# 使用"yes"和"no"启用/禁用。"auto"尝试使用 MPV 的 "platform" 该属性(mpv v0.36+)来决定
|
||||
# 如果该属性不可用,则默认为 "yes"
|
||||
normalise_backslash=auto
|
||||
|
||||
# 在`user-data`属性的`file_browser/open`字段中设置浏览器当前的打开状态
|
||||
# 此属性仅在 mpv v0.36+ 中可用
|
||||
set_user_data=yes
|
||||
|
||||
# 在`shared-script-properties`属性的`file_browser-open`字段中设置浏览器当前的打开状态
|
||||
# 该属性已被弃用
|
||||
set_shared_script_properties=no
|
||||
|
||||
####################################
|
||||
########## file overrides #########
|
||||
####################################
|
||||
|
||||
# directory 加载外部模块
|
||||
module_directory=~~/script-modules
|
||||
addon_directory=~~/script-modules/file-browser-addons
|
||||
custom_keybinds_file=~~/script-opts/file-browser-keybinds.json
|
||||
last_opened_directory_file=~~/files/file_browser-last_opened_directory
|
||||
|
||||
|
||||
####################################
|
||||
######### style settings ###########
|
||||
####################################
|
||||
|
||||
# 用"~/"在标题中替换用户的主目录,使用内部标签插件实现
|
||||
home_label=yes
|
||||
|
||||
# 设置文件浏览器以使用特定的文本对齐方式(默认:左上角)
|
||||
# 使用 ASS 标签对齐编号:https://aegi.vmoe.info/docs/3.0/ASS_Tags/#index23h3
|
||||
# 设置为 'auto' 以使用默认的 mpv osd 对齐选项
|
||||
# 选项:'auto'|'top'|'center'|'bottom'
|
||||
align_y=top
|
||||
# 选项: 'auto'|'left'|'center'|'right'
|
||||
align_x=left
|
||||
|
||||
# 用于标头的格式字符串。使用自定义键绑定替换代码
|
||||
# 动态更改标头的内容。请参阅:docs/custom-keybinds.md#codes
|
||||
# 例如,要添加文件编号请将其设置为: {\fnMonospace}[%i/%x]{\fn<font_name_header or blank>} %q\N----------------------------------------------------
|
||||
format_string_header=%q\N----------------------------------------------------
|
||||
|
||||
# 用于包装器的格式字符串。支持自定义键绑定替换代码,以及支持两个附加代码:'%<'和'%>',分别显示可见列表前后的项数
|
||||
# 将这些选项设置为空字符串将禁用包装器
|
||||
format_string_topwrapper=%< 项 覆盖\N
|
||||
format_string_bottomwrapper=\N%> 项 剩余
|
||||
|
||||
# 允许为光标和文件夹自定义图标,可以为矢量图形或 Unicode 字形。示例即为默认设置(矢量图形)
|
||||
#folder_icon={\p1}m 6.52 0 l 1.63 0 b 0.73 0 0.01 0.73 0.01 1.63 l 0 11.41 b 0 12.32 0.73 13.05 1.63 13.05 l 14.68 13.05 b 15.58 13.05 16.31 12.32 16.31 11.41 l 16.31 3.26 b 16.31 2.36 15.58 1.63 14.68 1.63 l 8.15 1.63{\p0}\h
|
||||
#cursor_icon={\p1}m 14.11 6.86 l 0.34 0.02 b 0.25 -0.02 0.13 -0 0.06 0.08 b -0.01 0.16 -0.02 0.28 0.04 0.36 l 3.38 5.55 l 3.38 5.55 3.67 6.15 3.81 6.79 3.79 7.45 3.61 8.08 3.39 8.5l 0.04 13.77 b -0.02 13.86 -0.01 13.98 0.06 14.06 b 0.11 14.11 0.17 14.13 0.24 14.13 b 0.27 14.13 0.31 14.13 0.34 14.11 l 14.11 7.28 b 14.2 7.24 14.25 7.16 14.25 7.07 b 14.25 6.98 14.2 6.9 14.11 6.86{\p0}\h
|
||||
#cursor_icon_flipped={\p1}m 0.13 6.86 l 13.9 0.02 b 14 -0.02 14.11 -0 14.19 0.08 b 14.26 0.16 14.27 0.28 14.21 0.36 l 10.87 5.55 l 10.87 5.55 10.44 6.79 10.64 8.08 10.86 8.5l 14.21 13.77 b 14.27 13.86 14.26 13.98 14.19 14.06 b 14.14 14.11 14.07 14.13 14.01 14.13 b 13.97 14.13 13.94 14.13 13.9 14.11 l 0.13 7.28 b 0.05 7.24 0 7.16 0 7.07 b 0 6.98 0.05 6.9 0.13 6.86{\p0}\h
|
||||
|
||||
# 设置字体的不透明度(十六进制),从 00(不透明)到 FF(透明)
|
||||
font_opacity_selection_marker=99
|
||||
|
||||
# 页眉使用粗体
|
||||
font_bold_header=yes
|
||||
|
||||
# 指定缩放浏览器的大小;2 会使大小增加一倍,0.5 会将其减半,依此类推。
|
||||
# header 和 wrappers 相对于 base 的大小进行缩放
|
||||
scaling_factor_base=1
|
||||
scaling_factor_header=1.4
|
||||
scaling_factor_wrappers=0.64
|
||||
|
||||
# 自定义字体名称,默认值为空白
|
||||
# 设置文件夹/光标的自定义字体可以修复损坏或丢失的图标
|
||||
#font_name_header=
|
||||
font_name_body=Noto Sans CJK SC,Noto Color Emoji
|
||||
#font_name_wrappers=
|
||||
#font_name_folder=
|
||||
#font_name_cursor=
|
||||
|
||||
# 自定义字体颜色
|
||||
# colours 采用十六进制格式,按蓝绿色红色顺序排列
|
||||
# 这与大多数 RGB 颜色代码的顺序相反
|
||||
font_colour_header=00ccff
|
||||
font_colour_body=ffffff
|
||||
font_colour_wrappers=00ccff
|
||||
font_colour_cursor=00ccff
|
||||
font_colour_escape_chars=413eff
|
||||
|
||||
# 以下选项是应用于不同状态的列表项的颜色
|
||||
font_colour_selected=fce788
|
||||
font_colour_multiselect=fcad88
|
||||
font_colour_playing=33ff66
|
||||
font_colour_playing_multiselected=22b547
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Oscar Manglaras
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,229 +0,0 @@
|
||||
# mpv-file-browser
|
||||
|
||||

|
||||
|
||||
This script allows users to browse and open files and folders entirely from within mpv. The script uses nothing outside the mpv API, so should work identically on all platforms. The browser can move up and down directories, start playing files and folders, or add them to the queue.
|
||||
|
||||
By default only file types compatible with mpv will be shown, but this can be changed in the config file.
|
||||
|
||||
This script requires at least **mpv v0.33**.
|
||||
|
||||
Originally, file-browser worked with versions of mpv going back to
|
||||
v0.31, you can find those older versions of file-browser in the
|
||||
[mpv-v0.31 branch](https://github.com/CogentRedTester/mpv-file-browser/tree/mpv-v0.31).
|
||||
That branch will no longer be receiving any feature updates,
|
||||
but I will try to fix any bugs that are reported on the issue
|
||||
tracker.
|
||||
|
||||
## Installation
|
||||
|
||||
### Basic
|
||||
|
||||
Clone this git repository into the mpv `~~/scripts` directory and
|
||||
change the name of the folder from `mpv-file-browser` to `file-browser`.
|
||||
You can then pull to receive updates.
|
||||
Alternatively, you can download the zip and extract the contents to `~~/scripts/file-browser`.
|
||||
`~~/` is the mpv config directory which is typically `~/.config/mpv/` on linux and `%APPDATA%/mpv/` on windows.
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a `file_browser.conf` file in the `~~/script-opts/` directory to configure the script.
|
||||
See [docs/file_browser.conf](docs/file_browser.conf) for the full list of options and their default values.
|
||||
The [`root` option](#root-directory) may be worth tweaking for your system.
|
||||
|
||||
### Addons
|
||||
|
||||
To use [addons](addons/README.md) place addon files in the `~~/script-modules/file-browser-addons/` directory.
|
||||
|
||||
### Custom Keybinds
|
||||
To setup [custom keybinds](docs/custom-keybinds.md) create a `~~/script-opts/file-browser-keybinds.json` file.
|
||||
Do **not** copy the `file-browser-keybinds.json` file
|
||||
stored in this repository, that file is a collection of random examples, many of which are for completely different
|
||||
operating systems. Use them and the [docs](docs/custom-keybinds.md) to create your own collection of keybinds.
|
||||
|
||||
### File Structure
|
||||
|
||||
<details>
|
||||
<summary>Expected directory tree (basic):</summary>
|
||||
|
||||
```
|
||||
~~/
|
||||
├── script-opts
|
||||
│ └── file_browser.conf
|
||||
└── scripts
|
||||
└── file-browser
|
||||
├── addons/
|
||||
├── docs/
|
||||
├── modules/
|
||||
├── screenshots/
|
||||
├── LICENSE
|
||||
├── main.lua
|
||||
└── README.md
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Expected directory tree (full):</summary>
|
||||
|
||||
```
|
||||
~~/
|
||||
├── script-modules
|
||||
│ └── file-browser-addons
|
||||
│ ├── addon1.lua
|
||||
│ ├── addon2.lua
|
||||
│ └── etc.lua
|
||||
├── script-opts
|
||||
│ ├── file_browser.conf
|
||||
│ └── file-browser-keybinds.json
|
||||
└── scripts
|
||||
└── file-browser
|
||||
├── addons/
|
||||
├── docs/
|
||||
├── modules/
|
||||
├── screenshots/
|
||||
├── LICENSE
|
||||
├── main.lua
|
||||
└── README.md
|
||||
```
|
||||
</details>
|
||||
|
||||
## Keybinds
|
||||
|
||||
The following keybinds are set by default
|
||||
|
||||
| Key | Name | Description |
|
||||
|-------------|----------------------------------|-------------------------------------------------------------------------------|
|
||||
| MENU | browse-files | toggles the browser |
|
||||
| Ctrl+o | open-browser | opens the browser |
|
||||
| Alt+o | browse-directory/get-user-input | opens a dialogue box to type in a directory - requires [mpv-user-input](#mpv-user-input) when mpv < v0.38 |
|
||||
|
||||
The following dynamic keybinds are only set while the browser is open:
|
||||
|
||||
| Key | Name | Description |
|
||||
|-------------|---------------|-------------------------------------------------------------------------------|
|
||||
| ESC | close | closes the browser or clears the selection |
|
||||
| ENTER | play | plays the currently selected file or folder |
|
||||
| Shift+ENTER | play_append | appends the current file or folder to the playlist |
|
||||
| Alt+ENTER | play_autoload | loads playlist entries before and after the selected file (like autoload.lua) |
|
||||
| RIGHT | down_dir | enter the currently selected directory |
|
||||
| LEFT | up_dir | move to the parent directory |
|
||||
| DOWN | scroll_down | move selector down the list |
|
||||
| UP | scroll_up | move selector up the list |
|
||||
| PGDWN | page_down | move selector down the list by a page (the num_entries option) |
|
||||
| PGUP | page_up | move selector up the list by a page (the num_entries option) |
|
||||
| Shift+PGDWN | list_bottom | move selector to the bottom of the list |
|
||||
| Shift+PGUP | list_top | move selector to the top of the list |
|
||||
| HOME | goto_current | move to the directory of the currently playing file |
|
||||
| Shift+HOME | goto_root | move to the root directory |
|
||||
| Alt+LEFT | history_back | move to previously open directory |
|
||||
| Alt+RIGHT | history_forward| move forwards again in history to the next directory |
|
||||
| Ctrl+r | reload | reload current directory |
|
||||
| Ctrl+Shift+r| cache/clear | clears the directory cache (disabled by default) |
|
||||
| s | select_mode | toggles multiselect mode |
|
||||
| S | select_item | toggles selection for the current item |
|
||||
| Ctrl+a | select_all | select all items in the current directory |
|
||||
| Ctrl+f | find/find | Opens a text input to search the contents of the folder - requires [mpv-user-input](#mpv-user-input) when mpv < v0.38|
|
||||
| Ctrl+F | find/find_advanced| Allows using [Lua Patterns](https://www.lua.org/manual/5.1/manual.html#5.4.1) in the search input|
|
||||
| n | find/next | Jumps to the next matching entry for the latest search term |
|
||||
| N | find/prev | Jumps to the previous matching entry for the latest search term |
|
||||
|
||||
When attempting to play or append a subtitle file the script will instead load the subtitle track into the existing video.
|
||||
|
||||
The behaviour of the autoload keybind can be reversed with the `autoload` script-opt.
|
||||
By default the playlist will only be autoloaded if `Alt+ENTER` is used on a single file, however when the option is switched autoload will always be used on single files *unless* `Alt+ENTER` is used. Using autoload on a directory, or while appending an item, will not work.
|
||||
|
||||
## Root Directory
|
||||
|
||||
To accomodate for both windows and linux this script has its own virtual root directory where drives and file folders can be manually added. The root directory can only contain folders.
|
||||
|
||||
The root directory is set using the `root` option, which is a comma separated list of directories. Entries are sent through mpv's `expand-path` command. By default `~/` and `C:/` are set on Windows
|
||||
and `~/` and `/` are set on non-Windows systems.
|
||||
Extra locations can be added manually, for example, my Windows root looks like:
|
||||
|
||||
`root=~/,C:/,D:/,E:/,Z:/`
|
||||
|
||||
## Multi-Select
|
||||
|
||||
By default file-browser only opens/appends the single item that the cursor has selected.
|
||||
However, using the `s` keybinds specified above, it is possible to select multiple items to open all at once. Selected items are shown in a different colour to the cursor.
|
||||
When in multiselect mode the cursor changes colour and scrolling up and down the list will drag the current selection. If the original item was unselected, then dragging will select items, if the original item was selected, then dragging will unselect items.
|
||||
|
||||
When multiple items are selected using the open or append commands all selected files will be added to the playlist in the order they appear on the screen.
|
||||
The currently selected (with the cursor) file will be ignored, instead the first multi-selected item in the folder will follow replace/append behaviour as normal, and following selected items will be appended to the playlist afterwards in the order that they appear on the screen.
|
||||
|
||||
## Custom Keybinds
|
||||
|
||||
File-browser also supports custom keybinds. These keybinds send normal input commands, but the script will substitute characters in the command strings for specific values depending on the currently open directory, and currently selected item.
|
||||
This allows for a wide range of customised behaviour, such as loading additional audio tracks from the browser, or copying the path of the selected item to the clipboard.
|
||||
|
||||
To see how to enable and use custom keybinds, see [custom-keybinds.md](docs/custom-keybinds.md).
|
||||
|
||||
## Add-ons
|
||||
|
||||
Add-ons are ways to add extra features to file-browser, for example adding support for network file servers like ftp, or implementing virtual directories in the root like recently opened files.
|
||||
They can be enabled by setting `addon` script-opt to yes, and placing the addon file into the `~~/script-modules/file-browser-addons/` directory.
|
||||
|
||||
For a list of existing addons see the [wiki](https://github.com/CogentRedTester/mpv-file-browser/wiki/Addon-List).
|
||||
For instructions on writing your own addons see [addons.md](docs/addons.md).
|
||||
|
||||
## Script Messages
|
||||
|
||||
File-browser supports a small number of script messages that allow the user or other scripts to talk with the browser.
|
||||
|
||||
### `browse-directory`
|
||||
|
||||
`script-message browse-directory [directory]`
|
||||
|
||||
Opens the given directory in the browser. If the browser is currently closed it will be opened.
|
||||
|
||||
### `get-directory-contents`
|
||||
|
||||
`script-message get-directory-contents [directory] [response-string]`
|
||||
|
||||
Reads the given directory, and sends the resulting tables to the specified script-message in the format:
|
||||
|
||||
`script-message [response-string] [list] [opts]`
|
||||
|
||||
The [list](docs/addons.md#the-list-array)
|
||||
and [opts](docs/addons.md#the-opts-table)
|
||||
tables are formatted as json strings through the `mp.utils.format_json` function.
|
||||
See [addons.md](docs/addons.md) for how the tables are structured, and what each field means.
|
||||
The API_VERSION field of the `opts` table refers to what version of the addon API file browser is using.
|
||||
The `response-string` refers to an arbitrary script-message that the tables should be sent to.
|
||||
|
||||
This script-message allows other scripts to utilise file-browser's directory parsing capabilities, as well as those of the file-browser addons.
|
||||
|
||||
## Conditional Auto-Profiles
|
||||
|
||||
file-browser provides a property that can be used with [conditional auto-profiles](https://mpv.io/manual/master/#conditional-auto-profiles)
|
||||
to detect when the browser is open.
|
||||
On mpv v0.36+ you should use the `user-data` property with the `file_browser/open` boolean.
|
||||
|
||||
Here is an example of an auto-profile that hides the OSC logo when using file-browser in an idle window:
|
||||
|
||||
```properties
|
||||
[hide-logo]
|
||||
profile-cond= idle_active and user_data.file_browser.open
|
||||
profile-restore=copy
|
||||
osc=no
|
||||
```
|
||||
|
||||
On older versions of mpv you can use the `file_browser-open` field of the `shared-script-properties` property:
|
||||
|
||||
```properties
|
||||
[hide-logo]
|
||||
profile-cond= idle_active and shared_script_properties["file_browser-open"] == "yes"
|
||||
profile-restore=copy
|
||||
osc=no
|
||||
```
|
||||
|
||||
See [#55](https://github.com/CogentRedTester/mpv-file-browser/issues/55) for more details on this.
|
||||
|
||||
## [mpv-user-input](https://github.com/CogentRedTester/mpv-user-input)
|
||||
|
||||
mpv-user-input is a script that provides an API to request text input from the user over the OSD.
|
||||
It was built using `console.lua` as a base, so supports almost all the same text input commands.
|
||||
If `user-input.lua` is loaded by mpv, and `user-input-module` is in the `~~/script-modules/` directory,
|
||||
then using `Alt+o` will open an input box that can be used to directly enter directories for file-browser to open.
|
||||
|
||||
Mpv v0.38 added the `mp.input` module, which means `mpv-user-input` is no-longer necessary from that version onwards.
|
||||
@@ -1,12 +0,0 @@
|
||||
# addons
|
||||
|
||||
Add-ons are ways to add extra features to file-browser, for example adding support for network file servers like ftp, or implementing virtual directories in the root like recently opened files.
|
||||
They can be enabled by setting `addon` script-opt to yes, and placing the addon file into the `~~/script-modules/file-browser-addons/` directory.
|
||||
|
||||
Browsing filesystems provided by add-ons should feel identical to the normal handling of the script,
|
||||
but they may require extra commandline tools be installed.
|
||||
|
||||
Since addons are loaded programatically from the addon directory it is possible for anyone to write their own addon.
|
||||
Instructions on how to do this are available [here](../docs/addons.md).
|
||||
|
||||
For a list of available addons see the [wiki](https://github.com/CogentRedTester/mpv-file-browser/wiki/Addon-List).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,330 +0,0 @@
|
||||
# Custom Keybinds
|
||||
|
||||
File-browser also supports custom keybinds. These keybinds send normal input commands, but the script will substitute characters in the command strings for specific values depending on the currently open directory, and currently selected item.
|
||||
This allows for a wide range of customised behaviour, such as loading additional audio tracks from the browser, or copying the path of the selected item to the clipboard.
|
||||
|
||||
The feature is disabled by default, but is enabled with the `custom_keybinds` script-opt.
|
||||
Keybinds are declared in the `~~/script-opts/file-browser-keybinds.json` file, the config takes the form of an array of json objects, with the following keys:
|
||||
|
||||
| option | required | default | description |
|
||||
|---------------|----------|------------|--------------------------------------------------------------------------------------------|
|
||||
| key | yes | - | the key to bind the command to - same syntax as input.conf |
|
||||
| command | yes | - | json array of commands and arguments |
|
||||
| name | no | numeric id | name of the script-binding - see [modifying default keybinds](#modifying-default-keybinds) |
|
||||
| condition | no | - | a Lua [expression](#expressions) - the keybind will only run if this evaluates to true |
|
||||
| flags | no | - | flags to send to the mpv add_keybind function - see [here](https://mpv.io/manual/master/#lua-scripting-[,flags]]\)) |
|
||||
| filter | no | - | run the command on just a file (`file`) or folder (`dir`) |
|
||||
| parser | no | - | run the command only in directories provided by the specified parser. |
|
||||
| multiselect | no | `false` | command is run on all selected items |
|
||||
| multi-type | no | `repeat` | which multiselect mode to use - `repeat` or `concat` |
|
||||
| delay | no | `0` | time to wait between sending repeated multi commands |
|
||||
| concat-string | no | `' '` (space) | string to insert between items when concatenating multi commands |
|
||||
| passthrough | no | - | force or ban passthrough behaviour - see [passthrough](#passthrough-keybinds) |
|
||||
| api_version | no | - | tie the keybind to a particular [addon API version](./addons.md#api-version), printing warnings and throwing errors if the keybind is used with wrong versions |
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["print-text", "example"],
|
||||
}
|
||||
```
|
||||
|
||||
The command can also be an array of arrays, in order to send multiple commands at once:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP2",
|
||||
"command": [
|
||||
["print-text", "example2"],
|
||||
["show-text", "example2"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Filter should not be included unless one wants to limit what types of list entries the command should be run on.
|
||||
To only run the command for directories use `dir`, to only run the command for files use `file`.
|
||||
|
||||
The parser filter is for filtering keybinds to only work inside directories loaded by specific parsers.
|
||||
There are two parsers in the base script, the default parser for native filesystems is called `file`, while the root parser is called `root`.
|
||||
Other parsers can be supplied by addons, and use the addon's filename with `-browser.lua` or just `.lua` stripped unless otherwise stated.
|
||||
For example `ftp-browser.lua` would have a parser called `ftp`.
|
||||
You can set the filter to match multiple parsers by separating the names with spaces.
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP2",
|
||||
"command": [ ["print-text", "example3"] ],
|
||||
"parser": "ftp file"
|
||||
}
|
||||
```
|
||||
|
||||
The `flags` field is mostly only useful for addons, but can also be useful if one wants a key to be repeatable.
|
||||
In this case the the keybind would look like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "p",
|
||||
"command": ["print-text", "spam-text"],
|
||||
"flags": { "repeatable": true }
|
||||
}
|
||||
```
|
||||
|
||||
## Codes
|
||||
|
||||
The script will scan every string in the command for the special substitution strings, they are:
|
||||
|
||||
| code | description |
|
||||
|--------|---------------------------------------------------------------------|
|
||||
| `%%` | escape code for `%` |
|
||||
| `%f` | filepath of the selected item |
|
||||
| `%n` | filename of the selected item |
|
||||
| `%p` | currently open directory |
|
||||
| `%q` | currently open directory but preferring the directory label |
|
||||
| `%d` | name of the current directory (characters between the last two '/') |
|
||||
| `%r` | name of the parser for the currently open directory |
|
||||
| `%x` | number of items in the currently open directory |
|
||||
| `%i` | the 1-based index of the selected item in the list |
|
||||
| `%j` | the 1-based index of the item in a multiselection - returns 1 for single selections |
|
||||
|
||||
Additionally, using the uppercase forms of those codes will send the substituted string through the `string.format("%q", str)` function.
|
||||
This adds double quotes around the string and automatically escapes any characters which would break the string encapsulation.
|
||||
This is not necessary for most mpv commands, but can be very useful when sending commands to the OS with the `run` command,
|
||||
or when passing values into [expressions](#conditional-command-condition-command).
|
||||
|
||||
Example of a command to add an audio track:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "Ctrl+a",
|
||||
"command": ["audio-add", "%f"],
|
||||
"filter": "file"
|
||||
}
|
||||
```
|
||||
|
||||
Any commands that contain codes representing specific items (`%f`, `%n`, `%i` etc) will
|
||||
not be run if no item is selected (for example in an empty directory).
|
||||
In these cases [passthrough](#passthrough-keybinds) rules will apply.
|
||||
|
||||
## Multiselect Commands
|
||||
|
||||
When multiple items are selected the command can be run for all items in the order they appear on the screen.
|
||||
This can be controlled by the `multiselect` flag, which takes a boolean value.
|
||||
When not set the flag defaults to `false`.
|
||||
|
||||
There are two different multiselect modes, controlled by the `multi-type` option. There are two options:
|
||||
|
||||
### `repeat`
|
||||
|
||||
The default mode that sends the commands once for each item that is selected.
|
||||
If time is needed between running commands of multiple selected items (for example, due to file handlers) then the `delay` option can be used to set a duration (in seconds) between commands.
|
||||
|
||||
### `concat`
|
||||
|
||||
Run a single command, but replace item specific codes with a concatenated string made from each selected item.
|
||||
For example `["print-text", "%n" ]` would print the name of each item selected separated by `' '` (space).
|
||||
The string inserted between each item is determined by the `concat-string` option, but `' '` is the default.
|
||||
|
||||
## Passthrough Keybinds
|
||||
|
||||
When loading keybinds from the json file file-browser will move down the list and overwrite any existing bindings with the same key.
|
||||
This means the lower an item on the list, the higher preference it has.
|
||||
However, file-browser implements a layered passthrough system for its keybinds; if a keybind is blocked from running by user filters, then the next highest preference command will be sent, continuing until a command is sent or there are no more keybinds.
|
||||
The default dynamic keybinds are considered the lowest priority.
|
||||
|
||||
The `filter`, `parser`, and `condition` options can all trigger passthrough, as well as some [codes](#codes).
|
||||
If a multi-select command is run on multiple items then passthrough will occur if any of the selected items fail the filters.
|
||||
|
||||
Passthrough can be forcibly disabled or enabled using the passthrough option.
|
||||
When set to `true` passthrough will always be activate regardless of the state of the filters.
|
||||
|
||||
## Modifying Default Keybinds
|
||||
|
||||
Since the custom keybinds are applied after the default dynamic keybinds they can be used to overwrite the default bindings.
|
||||
Setting new keys for the existing binds can be done with the `script-binding [binding-name]` command, where `binding-name` is the full name of the keybinding.
|
||||
For this script the names of the dynamic keybinds are in the format `file_browser/dynamic/[name]` where `name` is a unique identifier documented in the [keybinds](README.md#keybinds) table.
|
||||
|
||||
For example to change the scroll buttons from the arrows to the scroll wheel:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"key": "WHEEL_UP",
|
||||
"command": ["script-binding", "file_browser/dynamic/scroll_up"]
|
||||
},
|
||||
{
|
||||
"key": "WHEEL_DOWN",
|
||||
"command": ["script-binding", "file_browser/dynamic/scroll_down"]
|
||||
},
|
||||
{
|
||||
"key": "UP",
|
||||
"command": ["osd-auto", "add", "volume", "2"]
|
||||
},
|
||||
{
|
||||
"key": "DOWN",
|
||||
"command": ["osd-auto", "add", "volume", "-2"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Custom keybinds can be called using the same method, but users must set the `name` value inside the `file-browser-keybinds.json` file.
|
||||
To avoid conflicts custom keybinds use the format: `file_browser/dynamic/custom/[name]`.
|
||||
|
||||
## Expressions
|
||||
|
||||
Expressions are used to evaluate Lua code into a string that can be used for commands.
|
||||
These behave similarly to those used for [`profile-cond`](https://mpv.io/manual/master/#conditional-auto-profiles)
|
||||
values. In an expression the `mp`, `mp.msg`, and `mp.utils` modules are available as `mp`, `msg`, and `utils` respectively.
|
||||
Additionally, in mpv v0.38+ the `mp.input` module is available as `input`.
|
||||
|
||||
The file-browser [addon API](addons/addons.md#the-api) is available as `fb` and if [mpv-user-input](https://github.com/CogentRedTester/mpv-user-input)
|
||||
is installed then user-input API will be available in `user_input`.
|
||||
|
||||
This example only runs the keybind if the browser is in the Windows C drive or if
|
||||
the selected item is a matroska file:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["print-text", "in my C:/ drive!"],
|
||||
"condition": "(%P):find('C:/') == 1"
|
||||
},
|
||||
{
|
||||
"key": "KP2",
|
||||
"command": ["print-text", "Matroska File!"],
|
||||
"condition": "fb.get_extension(%N) == 'mkv'"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If the `condition` expression contains any item specific codes (`%F`, `%I`, etc) then it will be
|
||||
evaluated on each individual item, otherwise it will evaluated once for the whole keybind.
|
||||
If a code is invalid (for example using `%i` in empty directories) then the expression returns false.
|
||||
|
||||
There are some utility script messages that extend the power of expressions.
|
||||
[`conditional-command`](#conditional-command-condition-command) allows one to specify conditions that
|
||||
can apply to individual items or commands. The tradeoff is that you lose the automated passthrough behaviour.
|
||||
There is also [`evaluate-expressions`](#evaluate-expressions-command) which allows one to evaluate expressions inside commands.
|
||||
|
||||
## Utility Script Messages
|
||||
|
||||
There are a small number of custom script messages defined by file-browser to support custom keybinds.
|
||||
|
||||
### `=> <command...>`
|
||||
|
||||
A basic script message that makes it easier to chain multiple utility script messages together.
|
||||
Any `=>` string will be substituted for `script-message`.
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "=>", "delay-command", "%j * 2", "=>", "evaluate-expressions", "print-text", "!{%j * 2}"],
|
||||
"multiselect": true
|
||||
}
|
||||
```
|
||||
|
||||
### `conditional-command [condition] <command...>`
|
||||
|
||||
Runs the following command only if the condition [expression](#expressions) is `true`.
|
||||
|
||||
This example command will only run if the player is currently paused:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "conditional-command", "mp.get_property_bool('pause')", "print-text", "is paused"],
|
||||
}
|
||||
```
|
||||
|
||||
Custom keybind codes are evaluated before the expressions.
|
||||
|
||||
This example only runs if the currently selected item in the browser has a `.mkv` extension:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "conditional-command", "fb.get_extension(%N) == 'mkv'", "print-text", "a matroska file"],
|
||||
}
|
||||
```
|
||||
|
||||
### `delay-command [delay] <command...>`
|
||||
|
||||
Delays the following command by `[delay]` seconds.
|
||||
Delay is an [expression](#expressions).
|
||||
|
||||
The following example will send the `print-text` command after 5 seconds:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "delay-command", "5", "print-text", "example"],
|
||||
}
|
||||
```
|
||||
|
||||
### `evaluate-expressions <command...>`
|
||||
|
||||
Evaluates embedded Lua expressions in the following command.
|
||||
Expressions have the same behaviour as the [`conditional-command`](#conditional-command-condition-command) script-message.
|
||||
Expressions must be surrounded by `!{}` characters.
|
||||
Additional `!` characters can be placed at the start of the expression to
|
||||
escape the evaluation.
|
||||
|
||||
For example the following keybind will print 3 to the console:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "evaluate-expressions", "print-text", "!{1 + 2}"],
|
||||
}
|
||||
```
|
||||
|
||||
This example replaces all `/` characters in the path with `\`
|
||||
(note that the `\` needs to be escaped twice, once for the json file, and once for the string in the lua expression):
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "evaluate-expressions", "print-text", "!{ string.gsub(%F, '/', '\\\\') }"],
|
||||
}
|
||||
```
|
||||
|
||||
### `run-statement <statement...>`
|
||||
|
||||
Runs the following string a as a Lua statement. This is similar to an [expression](#expressions),
|
||||
but instead of the code evaluating to a value it must run a series of statements. Basically it allows
|
||||
for function bodies to be embedded into custom keybinds. All the same modules are available.
|
||||
If multiple strings are sent to the script-message then they will be concatenated together with newlines.
|
||||
|
||||
The following keybind will use [mpv-user-input](https://github.com/CogentRedTester/mpv-user-input) to
|
||||
rename items in file-browser:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "KP1",
|
||||
"command": ["script-message", "run-statement",
|
||||
"assert(user_input, 'install mpv-user-input!')",
|
||||
|
||||
"local line, err = user_input.get_user_input_co({",
|
||||
"id = 'rename-file',",
|
||||
"source = 'custom-keybind',",
|
||||
"request_text = 'rename file:',",
|
||||
"queueable = true,",
|
||||
"default_input = %N,",
|
||||
"cursor_pos = #(%N) - #fb.get_extension(%N, '')",
|
||||
"})",
|
||||
|
||||
"if not line then return end",
|
||||
"os.rename(%F, utils.join_path(%P, line))",
|
||||
|
||||
"fb.rescan()"
|
||||
],
|
||||
"parser": "file",
|
||||
"multiselect": true
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See [here](file-browser-keybinds.json).
|
||||
@@ -1,51 +0,0 @@
|
||||
[
|
||||
{
|
||||
"comment": "deletes the currently selected file",
|
||||
"key": "Alt+DEL",
|
||||
"command": ["script-message", "run-statement", "os.remove(%F) ; fb.rescan()"],
|
||||
"multiselect": true,
|
||||
"multi-type": "repeat"
|
||||
},
|
||||
{
|
||||
"comment": "opens the currently selected items in a new mpv window",
|
||||
"key": "Ctrl+ENTER",
|
||||
"command": ["run", "mpv", "%F"],
|
||||
"multiselect": true,
|
||||
"multi-type": "concat"
|
||||
},
|
||||
{
|
||||
"key": "Ctrl+c",
|
||||
"command": [
|
||||
["run", "powershell", "-command", "Set-Clipboard", "%F"],
|
||||
["print-text", "copied filepath to clipboard"]
|
||||
],
|
||||
"condition": "fb.get_platform() == 'windows'",
|
||||
"api_version": "1.9.0",
|
||||
"multiselect": true,
|
||||
"delay": 0.3
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"key": "WHEEL_UP",
|
||||
"command": ["script-binding", "file_browser/dynamic/scroll_up"],
|
||||
"flags": { "repeat": true }
|
||||
},
|
||||
{
|
||||
"key": "WHEEL_DOWN",
|
||||
"command": ["script-binding", "file_browser/dynamic/scroll_down"],
|
||||
"flags": { "repeat": true }
|
||||
},
|
||||
{
|
||||
"key": "MBTN_LEFT",
|
||||
"command": ["script-binding", "file_browser/dynamic/down_dir"]
|
||||
},
|
||||
{
|
||||
"key": "MBTN_RIGHT",
|
||||
"command": ["script-binding", "file_browser/dynamic/up_dir"]
|
||||
},
|
||||
{
|
||||
"key": "MBTN_MID",
|
||||
"command": ["script-binding", "file_browser/dynamic/play"]
|
||||
}
|
||||
]
|
||||
@@ -1,247 +0,0 @@
|
||||
#######################################################
|
||||
# This is the default config file for mpv-file-browser
|
||||
# https://github.com/CogentRedTester/mpv-file-browser
|
||||
#######################################################
|
||||
|
||||
####################################
|
||||
######## browser settings ##########
|
||||
####################################
|
||||
|
||||
# Root directories, separated by commas.
|
||||
# `C:/` and `/` are automatically added on Windows and non-windows systems, respectively.
|
||||
# The order of automatically added items can be changed by entering them here manually.
|
||||
root=~/
|
||||
|
||||
# characters to separate root directories, each character works individually
|
||||
root_separators=,
|
||||
|
||||
# number of entries to show on the screen at once
|
||||
num_entries=20
|
||||
|
||||
# number of directories to keep in the history.
|
||||
# A size of 0 disables the history.
|
||||
history_size=100
|
||||
|
||||
# wrap the cursor around the top and bottom of the list
|
||||
wrap=no
|
||||
|
||||
# enables loading external addons
|
||||
addons=yes
|
||||
|
||||
# enable custom keybinds
|
||||
# the keybind json file must go in ~~/script-opts
|
||||
custom_keybinds=yes
|
||||
|
||||
# Automatically detect windows drives and adds them to the root.
|
||||
# Using Ctrl+r in the root will run another scan.
|
||||
auto_detect_windows_drives=yes
|
||||
|
||||
# when opening the browser in idle mode prefer the current working directory over the root
|
||||
# note that the working directory is set as the 'current' directory regardless, so `home` will
|
||||
# move the browser there even if this option is set to false
|
||||
default_to_working_directory=no
|
||||
|
||||
# When opening the browser prefer the directory last opened by a previous mpv instance of file-browser.
|
||||
# Overrides the `default_to_working_directory` option.
|
||||
# Requires `save_last_opened_directory` to be `yes`.
|
||||
# Uses the internal `last-opened-directory` addon.
|
||||
default_to_last_opened_directory=no
|
||||
|
||||
# Whether to save the last opened directory.
|
||||
save_last_opened_directory=no
|
||||
|
||||
# Move the cursor to the currently playing item (if available) when the playing file changes.
|
||||
cursor_follows_playing_item=no
|
||||
|
||||
####################################
|
||||
########## filter settings #########
|
||||
####################################
|
||||
|
||||
# only show files compatible with mpv in the browser
|
||||
filter_files=yes
|
||||
|
||||
# file-browser only shows files that are compatible with mpv by default
|
||||
# adding a file extension to this list will add it to the extension whitelist
|
||||
# extensions are separated with commas, do not use any spaces
|
||||
extension_whitelist=
|
||||
|
||||
# add file extensions to this list to disable default filetypes
|
||||
# note that this will also override audio/subtitle_extension options below
|
||||
extension_blacklist=
|
||||
|
||||
# files with these extensions will be added as additional audio tracks for the current file instead of appended to the playlist
|
||||
# items on this list are automatically added to the extension whitelist
|
||||
audio_extensions=mka,dts,dtshd,dts-hd,truehd,true-hd
|
||||
|
||||
# files with these extensions will be added as additional subtitle tracks for the current file instead of appended to the playlist
|
||||
# items on this list are automatically added to the extension whitelist
|
||||
subtitle_extensions=etf,etf8,utf-8,idx,sub,srt,rt,ssa,ass,mks,vtt,sup,scc,smi,lrc,pgs
|
||||
|
||||
# filter directories or files starting with a period like .config for linux systems
|
||||
# auto will show dot entries on windows and hide them otherwise
|
||||
filter_dot_dirs=auto
|
||||
filter_dot_files=auto
|
||||
|
||||
####################################
|
||||
###### file loading settings #######
|
||||
####################################
|
||||
|
||||
# this option reverses the behaviour of the alt+ENTER keybind
|
||||
# when disabled the keybind is required to enable autoload for the file
|
||||
# when enabled the keybind disables autoload for the file
|
||||
autoload=no
|
||||
|
||||
# experimental feature that recurses directories concurrently when appending items to the playlist
|
||||
# this feature has the potential for massive performance improvements when using addons with asynchronous IO
|
||||
concurrent_recursion=yes
|
||||
|
||||
# maximum number of recursions that can run concurrently
|
||||
# if this number is too high it risks overflowing the mpv event queue, which will cause some directories to be dropped entirely
|
||||
max_concurrency=16
|
||||
|
||||
# substitute forward slashes for backslashes when appending a local file to the playlist
|
||||
# may be useful on windows systems
|
||||
substitute_backslash=no
|
||||
|
||||
# if autoload is triggered by selecting the currently playing file, then
|
||||
# the current file will have it's watch-later config saved before being closed and re-opened
|
||||
# essentially the current file will not be restarted
|
||||
autoload_save_current=yes
|
||||
|
||||
####################################
|
||||
### directory parsing settings #####
|
||||
####################################
|
||||
|
||||
# a directory cache to improve directory reading time,
|
||||
# enable if it takes a long time to load directories.
|
||||
# May cause 'ghost' files to be shown that no-longer exist or
|
||||
# fail to show files that have recently been created.
|
||||
# Reloading the directory with Ctrl+r will never use the cache.
|
||||
# Use Ctrl+Shift+r to forcibly clear the cache.
|
||||
cache=no
|
||||
|
||||
# Enables the internal `ls` addon that parses directories using the `ls` commandline tool.
|
||||
# Allows directory parsing to run concurrently, which prevents the browser from locking up.
|
||||
# Automatically disables itself on Windows systems.
|
||||
ls_parser=yes
|
||||
|
||||
# Enables the internal `windir` addon that parses directories using the `dir` command in cmd.exe.
|
||||
# Allows directory parsing to run concurrently, which prevents the browser from locking up.
|
||||
# Automatically disables itself on non-Windows systems.
|
||||
windir_parser=yes
|
||||
|
||||
# when moving up a directory do not stop on empty protocol schemes like `ftp://`
|
||||
# e.g. moving up from `ftp://localhost/` will move straight to the root instead of `ftp://`
|
||||
skip_protocol_schemes=yes
|
||||
|
||||
# map optical device paths to their respective file paths,
|
||||
# e.g. mapping bd:// to the value of the bluray-device property
|
||||
map_bd_device=yes
|
||||
map_dvd_device=yes
|
||||
map_cdda_device=yes
|
||||
|
||||
####################################
|
||||
########## misc settings ###########
|
||||
####################################
|
||||
|
||||
# turn the OSC idle screen off and on when opening and closing the browser
|
||||
# this should only be enabled if file-browser is the only thing controlling the idle-screen,
|
||||
# if multiple sources attempt to control the idle-screen at the same time it can cause unexpected behaviour.
|
||||
toggle_idlescreen=no
|
||||
|
||||
# interpret backslashes `\` in paths as forward slashes `/`
|
||||
# this is useful on Windows, which natively uses backslashes.
|
||||
# As backslashes are valid filename characters in Unix systems this could
|
||||
# cause mangled paths, though such filenames are rare.
|
||||
# Use `yes` and `no` to enable/disable. `auto` tries to use the mpv `platform`
|
||||
# property (mpv v0.36+) to decide. If the property is unavailable it defaults to `yes`.
|
||||
normalise_backslash=auto
|
||||
|
||||
# Set the current open status of the browser in the `file_browser/open` field of the `user-data` property.
|
||||
# This property is only available in mpv v0.36+.
|
||||
set_user_data=yes
|
||||
|
||||
# Set the current open status of the browser in the `file_browser-open` field of the `shared-script-properties` property.
|
||||
# This property is deprecated. When it is removed in mpv v0.37 file-browser will automatically disable this option.
|
||||
set_shared_script_properties=no
|
||||
|
||||
####################################
|
||||
########## file overrides #########
|
||||
####################################
|
||||
|
||||
# directory to load external modules - currently just user-input-module
|
||||
module_directory=~~/script-modules
|
||||
addon_directory=~~/script-modules/file-browser-addons
|
||||
custom_keybinds_file=~~/script-opts/file-browser-keybinds.json
|
||||
last_opened_directory_file=~~state/file_browser-last_opened_directory
|
||||
|
||||
####################################
|
||||
######### style settings ###########
|
||||
####################################
|
||||
|
||||
# Replace the user's home directory with `~/` in the header.
|
||||
# Uses the internal home-label addon.
|
||||
home_label=yes
|
||||
|
||||
# force file-browser to use a specific alignment (default: top-left)
|
||||
# set to auto to use the default mpv osd-align options
|
||||
# Options: 'auto'|'top'|'center'|'bottom'
|
||||
align_y=top
|
||||
# Options: 'auto'|'left'|'center'|'right'
|
||||
align_x=left
|
||||
|
||||
# The format string used for the header. Uses custom-keybind substitution codes to
|
||||
# dynamically change the contents of the header (see: docs/custom-keybinds.md#codes)
|
||||
# and supports the additional code `%^`which re-applies the default header ass style.
|
||||
# The original style used before the current one was: %q\N----------------------------------------------------
|
||||
format_string_header={\fnMonospace}[%i/%x]%^ %q\N------------------------------------------------------------------
|
||||
|
||||
# The format strings used for the wrappers. Supports custom-keybind substitution codes, and
|
||||
# supports two additional codes: `%<` and `%>` to show the number of items before and after the visible list, respectively.
|
||||
# Setting these options to empty strings will disable the wrappers.
|
||||
# Original styles used before the current ones were:
|
||||
# top: %< item(s) above\N
|
||||
# bottom: \N%> item(s) remaining
|
||||
format_string_topwrapper=...
|
||||
format_string_bottomwrapper=...
|
||||
|
||||
# allows custom icons be set for the folder and cursor
|
||||
# the `\h` character is a hard space to add padding
|
||||
folder_icon={\p1}m 6.52 0 l 1.63 0 b 0.73 0 0.01 0.73 0.01 1.63 l 0 11.41 b 0 12.32 0.73 13.05 1.63 13.05 l 14.68 13.05 b 15.58 13.05 16.31 12.32 16.31 11.41 l 16.31 3.26 b 16.31 2.36 15.58 1.63 14.68 1.63 l 8.15 1.63{\p0}\h
|
||||
cursor_icon={\p1}m 14.11 6.86 l 0.34 0.02 b 0.25 -0.02 0.13 -0 0.06 0.08 b -0.01 0.16 -0.02 0.28 0.04 0.36 l 3.38 5.55 l 3.38 5.55 3.67 6.15 3.81 6.79 3.79 7.45 3.61 8.08 3.39 8.5l 0.04 13.77 b -0.02 13.86 -0.01 13.98 0.06 14.06 b 0.11 14.11 0.17 14.13 0.24 14.13 b 0.27 14.13 0.31 14.13 0.34 14.11 l 14.11 7.28 b 14.2 7.24 14.25 7.16 14.25 7.07 b 14.25 6.98 14.2 6.9 14.11 6.86{\p0}\h
|
||||
cursor_icon_flipped={\p1}m 0.13 6.86 l 13.9 0.02 b 14 -0.02 14.11 -0 14.19 0.08 b 14.26 0.16 14.27 0.28 14.21 0.36 l 10.87 5.55 l 10.87 5.55 10.44 6.79 10.64 8.08 10.86 8.5l 14.21 13.77 b 14.27 13.86 14.26 13.98 14.19 14.06 b 14.14 14.11 14.07 14.13 14.01 14.13 b 13.97 14.13 13.94 14.13 13.9 14.11 l 0.13 7.28 b 0.05 7.24 0 7.16 0 7.07 b 0 6.98 0.05 6.9 0.13 6.86{\p0}\h
|
||||
|
||||
# set the opacity of fonts in hexadecimal from 00 (opaque) to FF (transparent)
|
||||
font_opacity_selection_marker=99
|
||||
|
||||
# print the header in bold font
|
||||
font_bold_header=yes
|
||||
|
||||
# scale the size of the browser; 2 would double the size, 0.5 would halve it, etc.
|
||||
# the header and wrapper scaling is relative to the base scaling
|
||||
scaling_factor_base=1
|
||||
scaling_factor_header=1.4
|
||||
scaling_factor_wrappers=1
|
||||
|
||||
# set custom font names, blank is the default
|
||||
# setting custom fonts for the folder/cursor can fix broken or missing icons
|
||||
font_name_header=
|
||||
font_name_body=
|
||||
font_name_wrappers=
|
||||
font_name_folder=
|
||||
font_name_cursor=
|
||||
|
||||
# set custom font colours
|
||||
# colours are in hexadecimal format in Blue Green Red order
|
||||
# note that this is the opposite order to RGB colour codes
|
||||
font_colour_header=00ccff
|
||||
font_colour_body=ffffff
|
||||
font_colour_wrappers=00ccff
|
||||
font_colour_cursor=00ccff
|
||||
font_colour_escape_chars=413eff
|
||||
|
||||
# these are colours applied to list items in different states
|
||||
font_colour_selected=fce788
|
||||
font_colour_multiselect=fcad88
|
||||
font_colour_playing=33ff66
|
||||
font_colour_playing_multiselected=22b547
|
||||
@@ -1,76 +0,0 @@
|
||||
--[[
|
||||
mpv-file-browser
|
||||
|
||||
This script allows users to browse and open files and folders entirely from within mpv.
|
||||
The script uses nothing outside the mpv API, so should work identically on all platforms.
|
||||
The browser can move up and down directories, start playing files and folders, or add them to the queue.
|
||||
|
||||
For full documentation see: https://github.com/CogentRedTester/mpv-file-browser
|
||||
]]--
|
||||
|
||||
local mp = require 'mp'
|
||||
|
||||
local o = require 'modules.options'
|
||||
|
||||
-- setting the package paths
|
||||
package.path = mp.command_native({"expand-path", o.module_directory}).."/?.lua;"..package.path
|
||||
|
||||
local addons = require 'modules.addons'
|
||||
local keybinds = require 'modules.keybinds'
|
||||
local setup = require 'modules.setup'
|
||||
local controls = require 'modules.controls'
|
||||
local observers = require 'modules.observers'
|
||||
local script_messages = require 'modules.script-messages'
|
||||
|
||||
local input_loaded, input = pcall(require, "mp.input")
|
||||
local user_input_loaded, user_input = pcall(require, "user-input-module")
|
||||
|
||||
|
||||
-- root and addon setup
|
||||
setup.root()
|
||||
addons.load_internal_addons()
|
||||
if o.addons then addons.load_external_addons() end
|
||||
addons.setup_addons()
|
||||
|
||||
--these need to be below the addon setup in case any parsers add custom entries
|
||||
setup.extensions_list()
|
||||
keybinds.setup_keybinds()
|
||||
|
||||
-- property observers
|
||||
mp.observe_property('path', 'string', observers.current_directory)
|
||||
if o.map_dvd_device then mp.observe_property('dvd-device', 'string', observers.dvd_device) end
|
||||
if o.map_bd_device then mp.observe_property('bluray-device', 'string', observers.bd_device) end
|
||||
if o.map_cdda_device then mp.observe_property('cdda-device', 'string', observers.cd_device) end
|
||||
if o.align_x == 'auto' then mp.observe_property('osd-align-x', 'string', observers.osd_align) end
|
||||
if o.align_y == 'auto' then mp.observe_property('osd-align-y', 'string', observers.osd_align) end
|
||||
|
||||
-- scripts messages
|
||||
mp.register_script_message('=>', script_messages.chain)
|
||||
mp.register_script_message('delay-command', script_messages.delay_command)
|
||||
mp.register_script_message('conditional-command', script_messages.conditional_command)
|
||||
mp.register_script_message('evaluate-expressions', script_messages.evaluate_expressions)
|
||||
mp.register_script_message('run-statement', script_messages.run_statement)
|
||||
|
||||
mp.register_script_message('browse-directory', controls.browse_directory)
|
||||
mp.register_script_message("get-directory-contents", script_messages.get_directory_contents)
|
||||
|
||||
--declares the keybind to open the browser
|
||||
mp.add_key_binding('MENU','browse-files', controls.toggle)
|
||||
mp.add_key_binding('Ctrl+o','open-browser', controls.open)
|
||||
|
||||
if input_loaded then
|
||||
mp.add_key_binding("Alt+o", "browse-directory/get-user-input", function()
|
||||
input.get({
|
||||
prompt = "open directory:",
|
||||
id = "file-browser/browse-directory",
|
||||
submit = function(text)
|
||||
controls.browse_directory(text)
|
||||
input.terminate()
|
||||
end
|
||||
})
|
||||
end)
|
||||
elseif user_input_loaded then
|
||||
mp.add_key_binding("Alt+o", "browse-directory/get-user-input", function()
|
||||
user_input.get_user_input(controls.browse_directory, {request_text = "open directory:"})
|
||||
end)
|
||||
end
|
||||
@@ -1,204 +0,0 @@
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local parser_API = require 'modules.apis.parser'
|
||||
|
||||
local API_MAJOR, API_MINOR, API_PATCH = g.API_VERSION:match("(%d+)%.(%d+)%.(%d+)")
|
||||
API_MAJOR, API_MINOR, API_PATCH = tonumber(API_MAJOR), tonumber(API_MINOR), tonumber(API_PATCH)
|
||||
|
||||
---checks if the given parser has a valid version number
|
||||
---@param parser Parser|Keybind
|
||||
---@param id string
|
||||
---@return boolean?
|
||||
local function check_api_version(parser, id)
|
||||
if parser.version then
|
||||
msg.warn(('%s: use of the `version` field is deprecated - use `api_version` instead'):format(id))
|
||||
parser.api_version = parser.version
|
||||
end
|
||||
|
||||
local version = parser.api_version
|
||||
if type(version) ~= 'string' then return msg.error(("%s: field `api_version` must be a string, got %s"):format(id, tostring(version))) end
|
||||
|
||||
local major, minor = version:match("(%d+)%.(%d+)")
|
||||
major, minor = tonumber(major), tonumber(minor)
|
||||
|
||||
if not major or not minor then
|
||||
return msg.error(("%s: invalid version number, expected v%d.%d.x, got v%s"):format(id, API_MAJOR, API_MINOR, version))
|
||||
elseif major ~= API_MAJOR then
|
||||
return msg.error(("%s has wrong major version number, expected v%d.x.x, got, v%s"):format(id, API_MAJOR, version))
|
||||
elseif minor > API_MINOR then
|
||||
msg.warn(("%s has newer minor version number than API, expected v%d.%d.x, got v%s"):format(id, API_MAJOR, API_MINOR, version))
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---create a unique id for the given parser
|
||||
---@param parser Parser
|
||||
local function set_parser_id(parser)
|
||||
local name = parser.name
|
||||
if g.parsers[name] then
|
||||
local n = 2
|
||||
name = parser.name.."_"..n
|
||||
while g.parsers[name] do
|
||||
n = n + 1
|
||||
name = parser.name.."_"..n
|
||||
end
|
||||
end
|
||||
|
||||
g.parsers[name] = parser
|
||||
g.parsers[parser] = { id = name }
|
||||
end
|
||||
|
||||
---runs an addon in a separate environment
|
||||
---@param path string
|
||||
---@return unknown
|
||||
local function run_addon(path)
|
||||
local name_sqbr = string.format("[%s]", path:match("/([^/]*)%.lua$"))
|
||||
local addon_environment = fb_utils.redirect_table(_G)
|
||||
addon_environment._G = addon_environment ---@diagnostic disable-line inject-field
|
||||
|
||||
--gives each addon custom debug messages
|
||||
addon_environment.package = fb_utils.redirect_table(addon_environment.package) ---@diagnostic disable-line inject-field
|
||||
addon_environment.package.loaded = fb_utils.redirect_table(addon_environment.package.loaded)
|
||||
local msg_module = {
|
||||
log = function(level, ...) msg.log(level, name_sqbr, ...) end,
|
||||
fatal = function(...) return msg.fatal(name_sqbr, ...) end,
|
||||
error = function(...) return msg.error(name_sqbr, ...) end,
|
||||
warn = function(...) return msg.warn(name_sqbr, ...) end,
|
||||
info = function(...) return msg.info(name_sqbr, ...) end,
|
||||
verbose = function(...) return msg.verbose(name_sqbr, ...) end,
|
||||
debug = function(...) return msg.debug(name_sqbr, ...) end,
|
||||
trace = function(...) return msg.trace(name_sqbr, ...) end,
|
||||
}
|
||||
addon_environment.print = msg_module.info ---@diagnostic disable-line inject-field
|
||||
|
||||
addon_environment.require = function(module) ---@diagnostic disable-line inject-field
|
||||
if module == "mp.msg" then return msg_module end
|
||||
return require(module)
|
||||
end
|
||||
|
||||
---@type function?, string?
|
||||
local chunk, err
|
||||
if setfenv then ---@diagnostic disable-line deprecated
|
||||
--since I stupidly named a function loadfile I need to specify the global one
|
||||
--I've been using the name too long to want to change it now
|
||||
chunk, err = _G.loadfile(path)
|
||||
if not chunk then return msg.error(err) end
|
||||
setfenv(chunk, addon_environment) ---@diagnostic disable-line deprecated
|
||||
else
|
||||
chunk, err = _G.loadfile(path, "bt", addon_environment) ---@diagnostic disable-line redundant-parameter
|
||||
if not chunk then return msg.error(err) end
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line no-unknown
|
||||
local success, result = xpcall(chunk, fb_utils.traceback)
|
||||
return success and result or nil
|
||||
end
|
||||
|
||||
---Setup an internal or external parser.
|
||||
---Note that we're somewhat bypassing the type system here as we're converting from a
|
||||
---ParserConfig object to a Parser object. As such we need to make sure that the
|
||||
---we're doing everything correctly. A 2.0 release of the addon API could simplify
|
||||
---this by formally separating ParserConfigs from Parsers and providing an
|
||||
---API to register parsers.
|
||||
---@param parser ParserConfig
|
||||
---@param file string
|
||||
---@return nil
|
||||
local function setup_parser(parser, file)
|
||||
parser = setmetatable(parser, { __index = parser_API }) --[[@as Parser]]
|
||||
parser.name = parser.name or file:gsub("%-browser%.lua$", ""):gsub("%.lua$", "")
|
||||
|
||||
set_parser_id(parser)
|
||||
if not check_api_version(parser, file) then return msg.error("aborting load of parser", parser:get_id(), "from", file) end
|
||||
|
||||
msg.verbose("imported parser", parser:get_id(), "from", file)
|
||||
|
||||
--sets missing functions
|
||||
if not parser.can_parse then
|
||||
if parser.parse then parser.can_parse = function() return true end
|
||||
else parser.can_parse = function() return false end end
|
||||
end
|
||||
|
||||
if parser.priority == nil then parser.priority = 0 end
|
||||
if type(parser.priority) ~= "number" then return msg.error("parser", parser:get_id(), "needs a numeric priority") end
|
||||
|
||||
table.insert(g.parsers, parser)
|
||||
end
|
||||
|
||||
---load an external addon
|
||||
---@param file string
|
||||
---@param path string
|
||||
---@return nil
|
||||
local function setup_addon(file, path)
|
||||
if file:sub(-4) ~= ".lua" then return msg.verbose(path, "is not a lua file - aborting addon setup") end
|
||||
|
||||
local addon_parsers = run_addon(path) --[=[@as ParserConfig|ParserConfig[]]=]
|
||||
if addon_parsers and not next(addon_parsers) then return msg.verbose('addon', path, 'returned empry table - special case, ignoring') end
|
||||
if not addon_parsers or type(addon_parsers) ~= "table" then return msg.error("addon", path, "did not return a table") end
|
||||
|
||||
--if the table contains a priority key then we assume it isn't an array of parsers
|
||||
if not addon_parsers[1] then addon_parsers = {addon_parsers} end
|
||||
|
||||
for _, parser in ipairs(addon_parsers --[=[@as ParserConfig[]]=]) do
|
||||
setup_parser(parser, file)
|
||||
end
|
||||
end
|
||||
|
||||
---loading external addons
|
||||
---@param directory string
|
||||
---@return nil
|
||||
local function load_addons(directory)
|
||||
directory = fb_utils.fix_path(directory, true)
|
||||
|
||||
local files = utils.readdir(directory)
|
||||
if not files then return msg.verbose('not loading external addons - could not read', o.addon_directory) end
|
||||
|
||||
for _, file in ipairs(files) do
|
||||
setup_addon(file, directory..file)
|
||||
end
|
||||
end
|
||||
|
||||
local function load_internal_addons()
|
||||
local script_dir = mp.get_script_directory()
|
||||
if not script_dir then return msg.error('script is not being run as a directory script!') end
|
||||
local internal_addon_dir = script_dir..'/modules/addons/'
|
||||
load_addons(internal_addon_dir)
|
||||
end
|
||||
|
||||
local function load_external_addons()
|
||||
local addon_dir = mp.command_native({"expand-path", o.addon_directory..'/'}) --[[@as string|nil]]
|
||||
if not addon_dir then return msg.verbose('not loading external addons - could not resolve', o.addon_directory) end
|
||||
load_addons(addon_dir)
|
||||
end
|
||||
|
||||
---Orders the addons by priority, sets the parser index values,
|
||||
---and runs the setup methods of the addons.
|
||||
local function setup_addons()
|
||||
table.sort(g.parsers, function(a, b) return a.priority < b.priority end)
|
||||
|
||||
--we want to store the indexes of the parsers
|
||||
for i = #g.parsers, 1, -1 do g.parsers[ g.parsers[i] ].index = i end
|
||||
|
||||
--we want to run the setup functions for each addon
|
||||
for index, parser in ipairs(g.parsers) do
|
||||
if parser.setup then
|
||||
local success = xpcall(function() parser:setup() end, fb_utils.traceback)
|
||||
if not success then
|
||||
msg.error("parser", parser:get_id(), "threw an error in the setup method - removing from list of parsers")
|
||||
table.remove(g.parsers, index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class addons
|
||||
return {
|
||||
check_api_version = check_api_version,
|
||||
load_internal_addons = load_internal_addons,
|
||||
load_external_addons = load_external_addons,
|
||||
setup_addons = setup_addons,
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
--[[
|
||||
This file is an internal file-browser addon.
|
||||
It should not be imported like a normal module.
|
||||
|
||||
Maintains a cache of the accessed directories to improve
|
||||
parsing speed. Disabled by default.
|
||||
]]
|
||||
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
local fb = require 'file-browser'
|
||||
|
||||
---@type ParserConfig
|
||||
local cacheParser = {
|
||||
name = 'cache',
|
||||
priority = 0,
|
||||
api_version = '1.9',
|
||||
}
|
||||
|
||||
---@class CacheEntry
|
||||
---@field list List
|
||||
---@field opts Opts?
|
||||
---@field timeout MPTimer
|
||||
|
||||
---@type table<string,CacheEntry>
|
||||
local cache = {}
|
||||
|
||||
---@type table<string,(async fun(list: List?, opts: Opts?))[]>
|
||||
local pending_parses = {}
|
||||
|
||||
---@param directories? string[]
|
||||
local function clear_cache(directories)
|
||||
if directories then
|
||||
msg.debug('clearing cache for', #directories, 'directorie(s)')
|
||||
for _, dir in ipairs(directories) do
|
||||
if cache[dir] then
|
||||
msg.trace('clearing cache for', dir)
|
||||
cache[dir].timeout:kill()
|
||||
cache[dir] = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
msg.debug('clearing cache')
|
||||
for _, entry in pairs(cache) do
|
||||
entry.timeout:kill()
|
||||
end
|
||||
cache = {}
|
||||
end
|
||||
end
|
||||
|
||||
---@type string
|
||||
local prev_directory = ''
|
||||
|
||||
function cacheParser:can_parse(directory, parse_state)
|
||||
-- allows the cache to be forcibly used or bypassed with the
|
||||
-- cache/use parse property.
|
||||
if parse_state.properties.cache and parse_state.properties.cache.use ~= nil then
|
||||
if parse_state.source == 'browser' then prev_directory = directory end
|
||||
return parse_state.properties.cache.use
|
||||
end
|
||||
|
||||
-- the script message is guaranteed to always bypass the cache
|
||||
if parse_state.source == 'script-message' then return false end
|
||||
if not fb.get_opt('cache') or directory == '' then return false end
|
||||
|
||||
-- clear the cache if reloading the current directory in the browser
|
||||
-- this means that fb.rescan() should maintain expected behaviour
|
||||
if parse_state.source == 'browser' then
|
||||
if prev_directory == directory then clear_cache({directory}) end
|
||||
prev_directory = directory
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@async
|
||||
function cacheParser:parse(directory)
|
||||
if cache[directory] then
|
||||
msg.verbose('fetching', directory, 'contents from cache')
|
||||
cache[directory].timeout:kill()
|
||||
cache[directory].timeout:resume()
|
||||
return cache[directory].list, cache[directory].opts
|
||||
end
|
||||
|
||||
---@type List?, Opts?
|
||||
local list, opts
|
||||
|
||||
-- if another parse is already running on the same directory, then wait and use the same result
|
||||
if not pending_parses[directory] then
|
||||
pending_parses[directory] = {}
|
||||
list, opts = self:defer(directory)
|
||||
else
|
||||
msg.debug('parse for', directory, 'already running - waiting for other parse to finish...')
|
||||
table.insert(pending_parses[directory], fb.coroutine.callback(30))
|
||||
list, opts = coroutine.yield()
|
||||
end
|
||||
|
||||
local pending = pending_parses[directory]
|
||||
-- need to clear the pending parses before resuming them or they will also attempt to resume the parses
|
||||
pending_parses[directory] = nil
|
||||
if pending and #pending > 0 then
|
||||
msg.debug('resuming', #pending, 'pending parses for', directory)
|
||||
for _, cb in ipairs(pending) do
|
||||
cb(list, opts)
|
||||
end
|
||||
end
|
||||
|
||||
if not list then return end
|
||||
|
||||
-- pending will be truthy for the original parse and falsy for any parses that were pending
|
||||
if pending then
|
||||
msg.debug('storing', directory, 'contents in cache')
|
||||
cache[directory] = {
|
||||
list = list,
|
||||
opts = opts,
|
||||
timeout = mp.add_timeout(120, function() cache[directory] = nil end),
|
||||
}
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
cacheParser.keybinds = {
|
||||
{
|
||||
key = 'Ctrl+Shift+r',
|
||||
name = 'clear',
|
||||
command = function() clear_cache() ; fb.rescan() end,
|
||||
}
|
||||
}
|
||||
|
||||
-- provide method of clearing the cache through script messages
|
||||
mp.register_script_message('cache/clear', function(dirs)
|
||||
if not dirs then
|
||||
return clear_cache()
|
||||
end
|
||||
|
||||
---@type string[]?
|
||||
local directories = utils.parse_json(dirs)
|
||||
if not directories then msg.error('unable to parse', dirs) end
|
||||
|
||||
clear_cache(directories)
|
||||
end)
|
||||
|
||||
return cacheParser
|
||||
@@ -1,46 +0,0 @@
|
||||
-- This file is an internal file-browser addon.
|
||||
-- It should not be imported like a normal module.
|
||||
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
---Parser for native filesystems
|
||||
---@type ParserConfig
|
||||
local file_parser = {
|
||||
name = "file",
|
||||
priority = 110,
|
||||
api_version = '1.0.0',
|
||||
}
|
||||
|
||||
--try to parse any directory except for the root
|
||||
function file_parser:can_parse(directory)
|
||||
return directory ~= ''
|
||||
end
|
||||
|
||||
--scans the given directory using the mp.utils.readdir function
|
||||
function file_parser:parse(directory)
|
||||
local new_list = {}
|
||||
local list1 = utils.readdir(directory, 'dirs')
|
||||
if list1 == nil then return nil end
|
||||
|
||||
--sorts folders and formats them into the list of directories
|
||||
for i=1, #list1 do
|
||||
local item = list1[i]
|
||||
|
||||
msg.trace(item..'/')
|
||||
table.insert(new_list, {name = item..'/', type = 'dir'})
|
||||
end
|
||||
|
||||
--appends files to the list of directory items
|
||||
local list2 = utils.readdir(directory, 'files')
|
||||
if list2 == nil then return nil end
|
||||
for i=1, #list2 do
|
||||
local item = list2[i]
|
||||
|
||||
msg.trace(item)
|
||||
table.insert(new_list, {name = item, type = 'file'})
|
||||
end
|
||||
return new_list
|
||||
end
|
||||
|
||||
return file_parser
|
||||
@@ -1,124 +0,0 @@
|
||||
--[[
|
||||
This file is an internal file-browser addon.
|
||||
It should not be imported like a normal module.
|
||||
|
||||
Allows searching the current directory.
|
||||
]]--
|
||||
|
||||
local msg = require "mp.msg"
|
||||
local fb = require "file-browser"
|
||||
local input_loaded, input = pcall(require, "mp.input")
|
||||
local user_input_loaded, user_input = pcall(require, "user-input-module")
|
||||
|
||||
---@type ParserConfig
|
||||
local find = {
|
||||
api_version = "1.3.0"
|
||||
}
|
||||
|
||||
---@type thread|nil
|
||||
local latest_coroutine = nil
|
||||
|
||||
---@type State
|
||||
local global_fb_state = getmetatable(fb.get_state()).__original
|
||||
|
||||
---@param name string
|
||||
---@param query string
|
||||
---@return boolean
|
||||
local function compare(name, query)
|
||||
if name:find(query) then return true end
|
||||
if name:lower():find(query) then return true end
|
||||
if name:upper():find(query) then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param key Keybind
|
||||
---@param state State
|
||||
---@param co thread
|
||||
---@return boolean?
|
||||
local function main(key, state, co)
|
||||
if not state.list then return false end
|
||||
|
||||
---@type string
|
||||
local text
|
||||
if key.name == "find/find" then text = "Find: enter search string"
|
||||
else text = "Find: enter advanced search string" end
|
||||
|
||||
if input_loaded then
|
||||
input.get({
|
||||
prompt = text .. "\n>",
|
||||
id = "file-browser/find",
|
||||
submit = fb.coroutine.callback(),
|
||||
})
|
||||
elseif user_input_loaded then
|
||||
user_input.get_user_input( fb.coroutine.callback(), { text = text, id = "find", replace = true } )
|
||||
end
|
||||
|
||||
local query, error = coroutine.yield()
|
||||
if input_loaded then input.terminate() end
|
||||
if not query then return msg.debug(error) end
|
||||
|
||||
-- allow the directory to be changed before this point
|
||||
local list = fb.get_list()
|
||||
local parse_id = global_fb_state.co
|
||||
|
||||
if key.name == "find/find" then
|
||||
query = fb.pattern_escape(query)
|
||||
end
|
||||
|
||||
local results = {}
|
||||
|
||||
for index, item in ipairs(list) do
|
||||
if compare(item.label or item.name, query) then
|
||||
table.insert(results, index)
|
||||
end
|
||||
end
|
||||
|
||||
if (#results < 1) then
|
||||
msg.warn("No matching items for '"..query.."'")
|
||||
return
|
||||
end
|
||||
|
||||
--keep cycling through the search results if any are found
|
||||
--putting this into a separate coroutine removes any passthrough ambiguity
|
||||
--the final return statement should return to `step_find` not any other function
|
||||
---@async
|
||||
fb.coroutine.run(function()
|
||||
latest_coroutine = coroutine.running()
|
||||
---@type number
|
||||
local rindex = 1
|
||||
while (true) do
|
||||
|
||||
if rindex == 0 then rindex = #results
|
||||
elseif rindex == #results + 1 then rindex = 1 end
|
||||
|
||||
fb.set_selected_index(results[rindex])
|
||||
local direction = coroutine.yield(true) --[[@as number]]
|
||||
rindex = rindex + direction
|
||||
|
||||
if parse_id ~= global_fb_state.co then
|
||||
latest_coroutine = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function step_find(key)
|
||||
if not latest_coroutine then return false end
|
||||
---@type number
|
||||
local direction = 0
|
||||
if key.name == "find/next" then direction = 1
|
||||
elseif key.name == "find/prev" then direction = -1 end
|
||||
return fb.coroutine.resume_err(latest_coroutine, direction)
|
||||
end
|
||||
|
||||
find.keybinds = {
|
||||
{"Ctrl+f", "find", main, {}},
|
||||
{"Ctrl+F", "find_advanced", main, {}},
|
||||
{"n", "next", step_find, {}},
|
||||
{"N", "prev", step_find, {}},
|
||||
}
|
||||
|
||||
return find
|
||||
@@ -1,31 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which displays ~/ for the home directory instead of the full path
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local fb = require "file-browser"
|
||||
|
||||
local home = fb.fix_path(mp.command_native({"expand-path", "~/"}) --[[@as string]], true)
|
||||
|
||||
---@type ParserConfig
|
||||
local home_label = {
|
||||
priority = 100,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
function home_label:can_parse(directory)
|
||||
if not fb.get_opt('home_label') then return false end
|
||||
return directory:sub(1, home:len()) == home
|
||||
end
|
||||
|
||||
---@async
|
||||
function home_label:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
if not opts then opts = {} end
|
||||
if (not opts.directory or opts.directory == directory) and not opts.directory_label then
|
||||
opts.directory_label = "~/"..(directory:sub(home:len()+1) or "")
|
||||
end
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return home_label
|
||||
@@ -1,62 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which stores the last opened directory and
|
||||
sets it as the opened directory the next time mpv is opened.
|
||||
|
||||
Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons
|
||||
]]--
|
||||
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local state_file = mp.command_native({'expand-path', fb.get_opt('last_opened_directory_file')}) --[[@as string]]
|
||||
msg.verbose('using', state_file)
|
||||
|
||||
---@param directory? string
|
||||
---@return nil
|
||||
local function write_directory(directory)
|
||||
if not fb.get_opt('save_last_opened_directory') then return end
|
||||
|
||||
local file = io.open(state_file, 'w+')
|
||||
|
||||
if not file then return msg.error('could not open', state_file, 'for writing') end
|
||||
|
||||
directory = directory or fb.get_directory() or ''
|
||||
msg.verbose('writing', directory, 'to', state_file)
|
||||
file:write(directory)
|
||||
file:close()
|
||||
end
|
||||
|
||||
---@type ParserConfig
|
||||
local addon = {
|
||||
api_version = '1.7.0',
|
||||
priority = 0,
|
||||
}
|
||||
|
||||
function addon:setup()
|
||||
if not fb.get_opt('default_to_last_opened_directory') then return end
|
||||
|
||||
local file = io.open(state_file, "r")
|
||||
if not file then
|
||||
return msg.info('failed to open', state_file, 'for reading (may be due to first load)')
|
||||
end
|
||||
|
||||
local dir = file:read("*a")
|
||||
msg.verbose('setting default directory to', dir)
|
||||
fb.browse_directory(dir, false)
|
||||
file:close()
|
||||
end
|
||||
|
||||
function addon:can_parse(dir, parse_state)
|
||||
if parse_state.source == 'browser' then write_directory(dir) end
|
||||
return false
|
||||
end
|
||||
|
||||
function addon:parse()
|
||||
return nil
|
||||
end
|
||||
|
||||
mp.register_event('shutdown', function() write_directory() end)
|
||||
|
||||
return addon
|
||||
@@ -1,68 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which uses the Linux ls command to parse native directories
|
||||
This behaves near identically to the native parser, but IO is done asynchronously.
|
||||
|
||||
Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
local fb = require "file-browser"
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
---@type ParserConfig
|
||||
local ls = {
|
||||
priority = 109,
|
||||
api_version = "1.9.0",
|
||||
name = "ls",
|
||||
keybind_name = "file"
|
||||
}
|
||||
|
||||
---@async
|
||||
---@param args string[]
|
||||
---@param parse_state ParseState
|
||||
---@return string|nil
|
||||
local function command(args, parse_state)
|
||||
local async = mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
args = args
|
||||
}, fb.coroutine.callback(30))
|
||||
|
||||
---@type boolean, boolean, MPVSubprocessResult
|
||||
local completed, _, cmd = parse_state:yield()
|
||||
if not completed then
|
||||
msg.warn('read timed out for:', table.unpack(args))
|
||||
mp.abort_async_command(async)
|
||||
return nil
|
||||
end
|
||||
|
||||
return cmd.status == 0 and cmd.stdout or nil
|
||||
end
|
||||
|
||||
function ls:can_parse(directory)
|
||||
if not fb.get_opt('ls_parser') then return false end
|
||||
return PLATFORM ~= 'windows' and directory ~= '' and not fb.get_protocol(directory)
|
||||
end
|
||||
|
||||
---@async
|
||||
function ls:parse(directory, parse_state)
|
||||
local list = {}
|
||||
local files = command({"ls", "-1", "-p", "-A", "-N", "--zero", "-L", directory}, parse_state)
|
||||
|
||||
if not files then return nil end
|
||||
|
||||
for str in files:gmatch("%Z+") do
|
||||
local is_dir = str:sub(-1) == "/"
|
||||
msg.trace(str)
|
||||
|
||||
table.insert(list, {name = str, type = is_dir and "dir" or "file"})
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
return ls
|
||||
@@ -1,26 +0,0 @@
|
||||
-- This file is an internal file-browser addon.
|
||||
-- It should not be imported like a normal module.
|
||||
|
||||
local g = require 'modules.globals'
|
||||
|
||||
---Parser for the root.
|
||||
---@type ParserConfig
|
||||
local root_parser = {
|
||||
name = "root",
|
||||
priority = math.huge,
|
||||
api_version = '1.0.0',
|
||||
}
|
||||
|
||||
function root_parser:can_parse(directory)
|
||||
return directory == ''
|
||||
end
|
||||
|
||||
--we return the root directory exactly as setup
|
||||
function root_parser:parse()
|
||||
return g.root, {
|
||||
sorted = true,
|
||||
filtered = true,
|
||||
}
|
||||
end
|
||||
|
||||
return root_parser
|
||||
@@ -1,218 +0,0 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which uses the Windows dir command to parse native directories
|
||||
This behaves near identically to the native parser, but IO is done asynchronously.
|
||||
|
||||
Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
local fb = require "file-browser"
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
---@param bytes string
|
||||
---@return fun(): number, number
|
||||
local function byte_iterator(bytes)
|
||||
---@async
|
||||
---@return number?
|
||||
local function iter()
|
||||
for i = 1, #bytes do
|
||||
coroutine.yield(bytes:byte(i), i)
|
||||
end
|
||||
error('malformed utf16le string - expected byte but found end of string')
|
||||
end
|
||||
|
||||
return coroutine.wrap(iter)
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param by number
|
||||
---@return number
|
||||
local function lshift(bits, by)
|
||||
return bits * 2^by
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param by number
|
||||
---@return integer
|
||||
local function rshift(bits, by)
|
||||
return math.floor(bits / 2^by)
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param i number
|
||||
---@return number
|
||||
local function bits_below(bits, i)
|
||||
return bits % 2^i
|
||||
end
|
||||
|
||||
---@param bits number
|
||||
---@param i number exclusive
|
||||
---@param j number inclusive
|
||||
---@return integer
|
||||
local function bits_between(bits, i, j)
|
||||
return rshift(bits_below(bits, j), i)
|
||||
end
|
||||
|
||||
---@param bytes string
|
||||
---@return number[]
|
||||
local function utf16le_to_unicode(bytes)
|
||||
msg.trace('converting from utf16-le to unicode codepoints')
|
||||
|
||||
---@type number[]
|
||||
local codepoints = {}
|
||||
|
||||
local get_byte = byte_iterator(bytes)
|
||||
|
||||
while true do
|
||||
-- start of a char
|
||||
local success, little, i = pcall(get_byte)
|
||||
if not success then break end
|
||||
|
||||
local big = get_byte()
|
||||
local codepoint = little + lshift(big, 8)
|
||||
|
||||
if codepoint < 0xd800 or codepoint > 0xdfff then
|
||||
table.insert(codepoints, codepoint)
|
||||
else
|
||||
-- handling surrogate pairs
|
||||
-- grab the next two bytes to grab the low surrogate
|
||||
local high_pair = codepoint
|
||||
local low_pair = get_byte() + lshift(get_byte(), 8)
|
||||
|
||||
if high_pair >= 0xdc00 then
|
||||
error(('malformed utf16le string at byte #%d (0x%04X) - high surrogate pair should be < 0xDC00'):format(i, high_pair))
|
||||
elseif low_pair < 0xdc00 then
|
||||
error(('malformed utf16le string at byte #%d (0x%04X) - low surrogate pair should be >= 0xDC00'):format(i+2, low_pair))
|
||||
end
|
||||
|
||||
-- The last 10 bits of each surrogate are the two halves of the codepoint
|
||||
-- https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
|
||||
local high_bits = bits_below(high_pair, 10)
|
||||
local low_bits = bits_below(low_pair, 10)
|
||||
local surrogate_par = (low_bits + lshift(high_bits, 10)) + 0x10000
|
||||
|
||||
table.insert(codepoints, surrogate_par)
|
||||
end
|
||||
end
|
||||
|
||||
return codepoints
|
||||
end
|
||||
|
||||
---@param codepoints number[]
|
||||
---@return string
|
||||
local function unicode_to_utf8(codepoints)
|
||||
---@type number[]
|
||||
local bytes = {}
|
||||
|
||||
-- https://en.wikipedia.org/wiki/UTF-8#Description
|
||||
for i, codepoint in ipairs(codepoints) do
|
||||
if codepoint >= 0xd800 and codepoint <= 0xdfff then
|
||||
error(('codepoint %d (U+%05X) is within the reserved surrogate pair range (U+D800-U+DFFF)'):format(i, codepoint))
|
||||
elseif codepoint <= 0x7f then
|
||||
table.insert(bytes, codepoint)
|
||||
elseif codepoint <= 0x7ff then
|
||||
table.insert(bytes, 0xC0 + rshift(codepoint, 6))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
elseif codepoint <= 0xffff then
|
||||
table.insert(bytes, 0xE0 + rshift(codepoint, 12))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
elseif codepoint <= 0x10ffff then
|
||||
table.insert(bytes, 0xF0 + rshift(codepoint, 18))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 12, 18))
|
||||
table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
|
||||
table.insert(bytes, 0x80 + bits_below(codepoint, 6))
|
||||
else
|
||||
error(('codepoint %d (U+%05X) is larger than U+10FFFF'):format(i, codepoint))
|
||||
end
|
||||
end
|
||||
|
||||
return string.char(table.unpack(bytes))
|
||||
end
|
||||
|
||||
local function utf8(text)
|
||||
return unicode_to_utf8(utf16le_to_unicode(text))
|
||||
end
|
||||
|
||||
---@type ParserConfig
|
||||
local dir = {
|
||||
priority = 109,
|
||||
api_version = "1.9.0",
|
||||
name = "cmd-dir",
|
||||
keybind_name = "file"
|
||||
}
|
||||
|
||||
---@async
|
||||
---@param args string[]
|
||||
---@param parse_state ParseState
|
||||
---@return string|nil
|
||||
local function command(args, parse_state)
|
||||
local async = mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
args = args,
|
||||
}, fb.coroutine.callback(30) )
|
||||
|
||||
---@type boolean, boolean, MPVSubprocessResult
|
||||
local completed, _, cmd = parse_state:yield()
|
||||
if not completed then
|
||||
msg.warn('read timed out for:', table.unpack(args))
|
||||
mp.abort_async_command(async)
|
||||
return nil
|
||||
end
|
||||
|
||||
local success = xpcall(function()
|
||||
cmd.stdout = utf8(cmd.stdout) or ''
|
||||
cmd.stderr = utf8(cmd.stderr) or ''
|
||||
end, fb.traceback)
|
||||
|
||||
if not success then return msg.error('failed to convert utf16-le string to utf8') end
|
||||
|
||||
--dir returns this exact error message if the directory is empty
|
||||
if cmd.status == 1 and cmd.stderr == "File Not Found\r\n" then cmd.status = 0 end
|
||||
if cmd.status ~= 0 then return msg.error(cmd.stderr) end
|
||||
|
||||
return cmd.status == 0 and cmd.stdout or nil
|
||||
end
|
||||
|
||||
function dir:can_parse(directory)
|
||||
if not fb.get_opt('windir_parser') then return false end
|
||||
return PLATFORM == 'windows' and directory ~= '' and not fb.get_protocol(directory)
|
||||
end
|
||||
|
||||
---@async
|
||||
function dir:parse(directory, parse_state)
|
||||
local list = {}
|
||||
|
||||
-- the dir command expects backslashes for our paths
|
||||
directory = string.gsub(directory, "/", "\\")
|
||||
|
||||
local dirs = command({ "cmd", "/U", "/c", "dir", "/b", "/ad", directory }, parse_state)
|
||||
if not dirs then return end
|
||||
|
||||
local files = command({ "cmd", "/U", "/c", "dir", "/b", "/a-d", directory }, parse_state)
|
||||
if not files then return end
|
||||
|
||||
for name in dirs:gmatch("[^\n\r]+") do
|
||||
name = name.."/"
|
||||
if fb.valid_dir(name) then
|
||||
table.insert(list, { name = name, type = "dir" })
|
||||
msg.trace(name)
|
||||
end
|
||||
end
|
||||
|
||||
for name in files:gmatch("[^\n\r]+") do
|
||||
if fb.valid_file(name) then
|
||||
table.insert(list, { name = name, type = "file" })
|
||||
msg.trace(name)
|
||||
end
|
||||
end
|
||||
|
||||
return list, { filtered = true }
|
||||
end
|
||||
|
||||
return dir
|
||||
@@ -1,62 +0,0 @@
|
||||
--[[
|
||||
This file is an internal file-browser addon.
|
||||
It should not be imported like a normal module.
|
||||
|
||||
Automatically populates the root with windows drives on startup.
|
||||
Ctrl+r will add new drives mounted since startup.
|
||||
|
||||
Drives will only be added if they are not already present in the root.
|
||||
]]
|
||||
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
---returns a list of windows drives
|
||||
---@return string[]?
|
||||
local function get_drives()
|
||||
---@type MPVSubprocessResult?, string?
|
||||
local result, err = mp.command_native({
|
||||
name = 'subprocess',
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
args = {'fsutil', 'fsinfo', 'drives'}
|
||||
})
|
||||
if not result then return msg.error(err) end
|
||||
if result.status ~= 0 then return msg.error('could not read windows root') end
|
||||
|
||||
local root = {}
|
||||
for drive in result.stdout:gmatch("(%a:)\\") do
|
||||
table.insert(root, drive..'/')
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
-- adds windows drives to the root if they are not already present
|
||||
local function import_drives()
|
||||
if fb.get_opt('auto_detect_windows_drives') and PLATFORM ~= 'windows' then return end
|
||||
|
||||
local drives = get_drives()
|
||||
if not drives then return end
|
||||
|
||||
for _, drive in ipairs(drives) do
|
||||
fb.register_root_item(drive)
|
||||
end
|
||||
end
|
||||
|
||||
local keybind = {
|
||||
key = 'Ctrl+r',
|
||||
name = 'import_root_drives',
|
||||
command = import_drives,
|
||||
parser = 'root',
|
||||
passthrough = true
|
||||
}
|
||||
|
||||
---@type ParserConfig
|
||||
return {
|
||||
api_version = '1.9.0',
|
||||
setup = import_drives,
|
||||
keybinds = { keybind }
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local ass = require 'modules.ass'
|
||||
local directory_movement = require 'modules.navigation.directory-movement'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
local controls = require 'modules.controls'
|
||||
|
||||
---@class FbAPI: fb_utils
|
||||
local fb = setmetatable({}, { __index = setmetatable({}, { __index = fb_utils }) })
|
||||
package.loaded["file-browser"] = setmetatable({}, { __index = fb })
|
||||
|
||||
--these functions we'll provide as-is
|
||||
fb.redraw = ass.update_ass
|
||||
fb.browse_directory = controls.browse_directory
|
||||
|
||||
---Clears the directory cache.
|
||||
---@return thread
|
||||
function fb.rescan()
|
||||
return scanning.rescan()
|
||||
end
|
||||
|
||||
---@async
|
||||
---@return thread
|
||||
function fb.rescan_await()
|
||||
local co = scanning.rescan(nil, fb_utils.coroutine.callback())
|
||||
coroutine.yield()
|
||||
return co
|
||||
end
|
||||
|
||||
---@param directories? string[]
|
||||
function fb.clear_cache(directories)
|
||||
if directories then
|
||||
mp.commandv('script-message-to', mp.get_script_name(), 'cache/clear', utils.format_json(directories))
|
||||
else
|
||||
mp.commandv('script-message-to', mp.get_script_name(), 'cache/clear')
|
||||
end
|
||||
end
|
||||
|
||||
---A wrapper around scan_directory for addon API.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param parse_state ParseStateTemplate
|
||||
---@return Item[]|nil
|
||||
---@return Opts
|
||||
function fb.parse_directory(directory, parse_state)
|
||||
if not parse_state then parse_state = { source = "addon" }
|
||||
elseif not parse_state.source then parse_state.source = "addon" end
|
||||
return scanning.scan_directory(directory, parse_state)
|
||||
end
|
||||
|
||||
---Register file extensions which can be opened by the browser.
|
||||
---@param ext string
|
||||
function fb.register_parseable_extension(ext)
|
||||
g.parseable_extensions[string.lower(ext)] = true
|
||||
end
|
||||
|
||||
---Deregister file extensions which can be opened by the browser.
|
||||
---@param ext string
|
||||
function fb.remove_parseable_extension(ext)
|
||||
g.parseable_extensions[string.lower(ext)] = nil
|
||||
end
|
||||
|
||||
---Add a compatible extension to show through the filter, only applies if run during the setup() method.
|
||||
---@param ext string
|
||||
function fb.add_default_extension(ext)
|
||||
table.insert(g.compatible_file_extensions, ext)
|
||||
end
|
||||
|
||||
---Add item to root at position pos.
|
||||
---@param item Item
|
||||
---@param pos? number
|
||||
function fb.insert_root_item(item, pos)
|
||||
msg.debug("adding item to root", item.label or item.name, pos)
|
||||
item.ass = item.ass or fb.ass_escape(item.label or item.name)
|
||||
item.type = "dir"
|
||||
table.insert(g.root, pos or (#g.root + 1), item)
|
||||
end
|
||||
|
||||
---Add a new mapping to the given directory.
|
||||
---@param directory string
|
||||
---@param mapping string
|
||||
---@param pattern? boolean
|
||||
---@return string
|
||||
function fb.register_directory_mapping(directory, mapping, pattern)
|
||||
if not pattern then mapping = '^'..fb_utils.pattern_escape(mapping) end
|
||||
g.directory_mappings[mapping] = directory
|
||||
msg.verbose('registering directory alias', mapping, directory)
|
||||
|
||||
directory_movement.set_current_file(g.current_file.original_path)
|
||||
return mapping
|
||||
end
|
||||
|
||||
---Remove all directory mappings that map to the given directory.
|
||||
---@param directory string
|
||||
---@return string[]
|
||||
function fb.remove_all_mappings(directory)
|
||||
local removed = {}
|
||||
for mapping, target in pairs(g.directory_mappings) do
|
||||
if target == directory then
|
||||
g.directory_mappings[mapping] = nil
|
||||
table.insert(removed, mapping)
|
||||
end
|
||||
end
|
||||
return removed
|
||||
end
|
||||
|
||||
---A newer API for adding items to the root.
|
||||
---Only adds the item if the same item does not already exist in the root.
|
||||
---@param item Item|string
|
||||
---@param priority? number Specifies the insertion location, a lower priority
|
||||
--- is placed higher in the list and the default is 100.
|
||||
---@return boolean
|
||||
function fb.register_root_item(item, priority)
|
||||
msg.verbose('registering root item:', utils.to_string(item))
|
||||
if type(item) == 'string' then
|
||||
item = {name = item, type = 'dir'}
|
||||
end
|
||||
|
||||
-- if the item is already in the list then do nothing
|
||||
if fb.list.some(g.root, function(r)
|
||||
return fb.get_full_path(r, '') == fb.get_full_path(item, '')
|
||||
end) then return false end
|
||||
|
||||
---@type table<Item,number>
|
||||
local priorities = {}
|
||||
|
||||
priorities[item] = priority
|
||||
for i, v in ipairs(g.root) do
|
||||
if (priorities[v] or 100) > (priority or 100) then
|
||||
fb.insert_root_item(item, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
fb.insert_root_item(item)
|
||||
return true
|
||||
end
|
||||
|
||||
--providing getter and setter functions so that addons can't modify things directly
|
||||
|
||||
|
||||
---@param key string
|
||||
---@return boolean|string|number
|
||||
function fb.get_opt(key) return o[key] end
|
||||
|
||||
function fb.get_script_opts() return fb.copy_table(o) end
|
||||
function fb.get_platform() return g.PLATFORM end
|
||||
function fb.get_extensions() return fb.copy_table(g.extensions) end
|
||||
function fb.get_sub_extensions() return fb.copy_table(g.sub_extensions) end
|
||||
function fb.get_audio_extensions() return fb.copy_table(g.audio_extensions) end
|
||||
function fb.get_parseable_extensions() return fb.copy_table(g.parseable_extensions) end
|
||||
function fb.get_state() return fb.copy_table(g.state) end
|
||||
function fb.get_parsers() return fb.copy_table(g.parsers) end
|
||||
function fb.get_root() return fb.copy_table(g.root) end
|
||||
function fb.get_directory() return g.state.directory end
|
||||
function fb.get_list() return fb.copy_table(g.state.list) end
|
||||
function fb.get_current_file() return fb.copy_table(g.current_file) end
|
||||
function fb.get_current_parser() return g.state.parser:get_id() end
|
||||
function fb.get_current_parser_keyname() return g.state.parser.keybind_name or g.state.parser.name end
|
||||
function fb.get_selected_index() return g.state.selected end
|
||||
function fb.get_selected_item() return fb.copy_table(g.state.list[g.state.selected]) end
|
||||
function fb.get_open_status() return not g.state.hidden end
|
||||
function fb.get_parse_state(co) return g.parse_states[co or coroutine.running() or ""] end
|
||||
function fb.get_history() return fb.copy_table(g.history.list) end
|
||||
function fb.get_history_index() return g.history.position end
|
||||
|
||||
---@deprecated
|
||||
---@return string|nil
|
||||
function fb.get_dvd_device()
|
||||
local dvd_device = mp.get_property('dvd-device')
|
||||
if not dvd_device or dvd_device == '' then return nil end
|
||||
return fb_utils.fix_path(dvd_device, true)
|
||||
end
|
||||
|
||||
---@param str string
|
||||
function fb.set_empty_text(str)
|
||||
g.state.empty_text = str
|
||||
fb.redraw()
|
||||
end
|
||||
|
||||
---@param index number
|
||||
---@return number|false
|
||||
function fb.set_selected_index(index)
|
||||
if type(index) ~= "number" then return false end
|
||||
if index < 1 then index = 1 end
|
||||
if index > #g.state.list then index = #g.state.list end
|
||||
g.state.selected = index
|
||||
fb.redraw()
|
||||
return index
|
||||
end
|
||||
|
||||
fb.set_history_index = directory_movement.goto_history
|
||||
|
||||
return fb
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
local msg = require 'mp.msg'
|
||||
|
||||
local g = require 'modules.globals'
|
||||
|
||||
---@class ParseStateAPI
|
||||
local parse_state_API = {}
|
||||
|
||||
---A wrapper around coroutine.yield that aborts the coroutine if
|
||||
--the parse request was cancelled by the user.
|
||||
--the coroutine is
|
||||
---@async
|
||||
---@param self ParseState
|
||||
---@param ... any
|
||||
---@return unknown ...
|
||||
function parse_state_API:yield(...)
|
||||
local co = coroutine.running()
|
||||
local is_browser = co == g.state.co
|
||||
|
||||
local result = table.pack(coroutine.yield(...))
|
||||
if is_browser and co ~= g.state.co then
|
||||
msg.verbose("browser no longer waiting for list - aborting parse for", self.directory)
|
||||
error(g.ABORT_ERROR)
|
||||
end
|
||||
return table.unpack(result, 1, result.n)
|
||||
end
|
||||
|
||||
---Checks if the current coroutine is the one handling the browser's request.
|
||||
---@return boolean
|
||||
function parse_state_API:is_coroutine_current()
|
||||
return coroutine.running() == g.state.co
|
||||
end
|
||||
|
||||
return parse_state_API
|
||||
@@ -1,40 +0,0 @@
|
||||
local msg = require 'mp.msg'
|
||||
|
||||
local g = require 'modules.globals'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
local fb = require 'modules.apis.fb'
|
||||
|
||||
---@class ParserAPI: FbAPI
|
||||
local parser_api = setmetatable({}, { __index = fb })
|
||||
|
||||
---Returns the index of the parser.
|
||||
---@return number
|
||||
function parser_api:get_index() return g.parsers[self].index end
|
||||
|
||||
---Returns the ID of the parser
|
||||
---@return string
|
||||
function parser_api:get_id() return g.parsers[self].id end
|
||||
|
||||
---A newer API for adding items to the root.
|
||||
---Only adds the item if the same item does not already exist in the root.
|
||||
---Wrapper around `fb.register_root_item`.
|
||||
---@param item Item|string
|
||||
---@param priority? number The priority for the added item. Uses the parsers priority by default.
|
||||
---@return boolean
|
||||
function parser_api:register_root_item(item, priority)
|
||||
return fb.register_root_item(item, priority or g.parsers[self:get_id()].priority)
|
||||
end
|
||||
|
||||
---Runs choose_and_parse starting from the next parser.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@return Item[]?
|
||||
---@return Opts?
|
||||
function parser_api:defer(directory)
|
||||
msg.trace("deferring to other parsers...")
|
||||
local list, opts = scanning.choose_and_parse(directory, self:get_index() + 1)
|
||||
fb.get_parse_state().already_deferred = true
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return parser_api
|
||||
@@ -1,238 +0,0 @@
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
-----------------------------------------List Formatting------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
|
||||
local g = require 'modules.globals'
|
||||
local o = require 'modules.options'
|
||||
local fb_utils = require 'modules.utils'
|
||||
|
||||
local state = g.state
|
||||
local style = g.style
|
||||
local ass = g.ass
|
||||
|
||||
--- https://www.unicode.org/reports/tr9/#Explicit_Directional_Isolates
|
||||
local ISOLATE_DIRECTION_START = '\226\129\168' -- U+2068 FIRST STRONG ISOLATE
|
||||
local ISOLATE_DIRECTION_END = '\226\129\169' -- U+2069 POP DIRECTIONAL ISOLATE
|
||||
|
||||
local function draw()
|
||||
ass:update()
|
||||
end
|
||||
|
||||
local function remove()
|
||||
ass:remove()
|
||||
end
|
||||
|
||||
---@type string[]
|
||||
local string_buffer = {}
|
||||
|
||||
---appends the entered text to the overlay
|
||||
---@param ... string
|
||||
local function append(...)
|
||||
for i = 1, select("#", ...) do
|
||||
table.insert(string_buffer, select(i, ...) or '' )
|
||||
end
|
||||
end
|
||||
|
||||
--appends a newline character to the osd
|
||||
local function newline()
|
||||
table.insert(string_buffer, '\\N')
|
||||
end
|
||||
|
||||
local function flush_buffer()
|
||||
ass.data = table.concat(string_buffer, '')
|
||||
string_buffer = {}
|
||||
end
|
||||
|
||||
---detects whether or not to highlight the given entry as being played
|
||||
---@param v Item
|
||||
---@return boolean
|
||||
local function highlight_entry(v)
|
||||
if g.current_file.path == nil then return false end
|
||||
local full_path = fb_utils.get_full_path(v)
|
||||
local alt_path = v.name and g.state.directory..v.name or nil
|
||||
|
||||
if fb_utils.parseable_item(v) then
|
||||
return (
|
||||
string.find(g.current_file.directory, full_path, 1, true)
|
||||
or (alt_path and string.find(g.current_file.directory, alt_path, 1, true))
|
||||
) ~= nil
|
||||
else
|
||||
return g.current_file.path == full_path
|
||||
or (alt_path and g.current_file.path == alt_path)
|
||||
end
|
||||
end
|
||||
|
||||
---Escapes unwanted unicode control characters that may affect the rest of the display.
|
||||
---Currently this only isolates unicode directional overrides.
|
||||
---Based on: https://github.com/mpv-player/mpv/pull/17606
|
||||
---@param str string
|
||||
---@return string
|
||||
local function unicode_escape(str)
|
||||
return ISOLATE_DIRECTION_START..str..ISOLATE_DIRECTION_END
|
||||
end
|
||||
|
||||
---escape ass values and replace newlines
|
||||
---@param str string
|
||||
---@param style_reset string?
|
||||
---@return string
|
||||
local function ass_escape(str, style_reset)
|
||||
return fb_utils.ass_escape(str, style_reset and style.warning..'␊'..style_reset or true)
|
||||
end
|
||||
|
||||
local header_overrides = {['^'] = style.header}
|
||||
|
||||
---@return number start
|
||||
---@return number finish
|
||||
---@return boolean is_overflowing
|
||||
local function calculate_view_window()
|
||||
---@type number
|
||||
local start = 1
|
||||
---@type number
|
||||
local finish = start+o.num_entries-1
|
||||
|
||||
--handling cursor positioning
|
||||
local mid = math.ceil(o.num_entries/2)+1
|
||||
if state.selected+mid > finish then
|
||||
---@type number
|
||||
local offset = state.selected - finish + mid
|
||||
|
||||
--if we've overshot the end of the list then undo some of the offset
|
||||
if finish + offset > #state.list then
|
||||
offset = offset - ((finish+offset) - #state.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 < #state.list
|
||||
--this is necessary when the number of items in the dir is less than the max
|
||||
if not overflow then finish = #state.list end
|
||||
|
||||
return start, finish, overflow
|
||||
end
|
||||
|
||||
---@param i number index
|
||||
---@return string
|
||||
local function calculate_item_style(i)
|
||||
local is_playing_file = highlight_entry(state.list[i])
|
||||
|
||||
--sets the selection colour scheme
|
||||
local multiselected = state.selection[i]
|
||||
|
||||
--sets the colour for the item
|
||||
local item_style = style.body
|
||||
|
||||
if multiselected then item_style = item_style..style.multiselect
|
||||
elseif i == state.selected then item_style = item_style..style.selected end
|
||||
|
||||
if is_playing_file then item_style = item_style..(multiselected and style.playing_selected or style.playing) end
|
||||
|
||||
return item_style
|
||||
end
|
||||
|
||||
local function draw_header()
|
||||
append(style.header)
|
||||
append(fb_utils.substitute_codes(o.format_string_header, header_overrides, nil, nil, function(str, code)
|
||||
if code == '^' then return str end
|
||||
return ass_escape(str, style.header)
|
||||
end))
|
||||
newline()
|
||||
end
|
||||
|
||||
---@param wrapper_overrides ReplacerTable
|
||||
local function draw_top_wrapper(wrapper_overrides)
|
||||
--adding a header to show there are items above in the list
|
||||
append(style.footer_header)
|
||||
append(fb_utils.substitute_codes(o.format_string_topwrapper, wrapper_overrides, nil, nil, function(str)
|
||||
return ass_escape(str)
|
||||
end))
|
||||
newline()
|
||||
end
|
||||
|
||||
---@param wrapper_overrides ReplacerTable
|
||||
local function draw_bottom_wrapper(wrapper_overrides)
|
||||
append(style.footer_header)
|
||||
append(fb_utils.substitute_codes(o.format_string_bottomwrapper, wrapper_overrides, nil, nil, function(str)
|
||||
return ass_escape(str)
|
||||
end))
|
||||
end
|
||||
|
||||
---@param i number index
|
||||
---@param cursor string
|
||||
local function draw_cursor(i, cursor)
|
||||
--handles custom styles for different entries
|
||||
if i == state.selected or i == state.multiselect_start then
|
||||
if not (i == state.selected) then append(style.selection_marker) end
|
||||
|
||||
if not state.multiselect_start then append(style.cursor)
|
||||
else
|
||||
if state.selection[state.multiselect_start] then append(style.cursor_select)
|
||||
else append(style.cursor_deselect) end
|
||||
end
|
||||
else
|
||||
append(g.style.indent)
|
||||
end
|
||||
append(cursor, '\\h', style.body)
|
||||
end
|
||||
|
||||
--refreshes the ass text using the contents of the list
|
||||
local function update_ass()
|
||||
if state.hidden then state.flag_update = true ; return end
|
||||
|
||||
append(style.global)
|
||||
draw_header()
|
||||
|
||||
if #state.list < 1 then
|
||||
append(state.empty_text)
|
||||
flush_buffer()
|
||||
draw()
|
||||
return
|
||||
end
|
||||
|
||||
local start, finish, overflow = calculate_view_window()
|
||||
|
||||
-- these are the number values to place into the wrappers
|
||||
local wrapper_overrides = {['<'] = tostring(start-1), ['>'] = tostring(#state.list-finish)}
|
||||
if o.format_string_topwrapper ~= '' and start > 1 then
|
||||
draw_top_wrapper(wrapper_overrides)
|
||||
end
|
||||
|
||||
for i=start, finish do
|
||||
local v = state.list[i]
|
||||
append(style.body)
|
||||
if g.ALIGN_X ~= 'right' then draw_cursor(i, o.cursor_icon) end
|
||||
|
||||
local item_style = calculate_item_style(i)
|
||||
append(item_style)
|
||||
|
||||
--sets the folder icon
|
||||
if v.type == 'dir' then
|
||||
append(style.folder, o.folder_icon, "\\h", style.body)
|
||||
append(item_style)
|
||||
end
|
||||
|
||||
--adds the actual name of the item
|
||||
append(v.ass or ass_escape( unicode_escape(v.label or v.name) , item_style), '\\h')
|
||||
if g.ALIGN_X == 'right' then draw_cursor(i, o.cursor_icon_flipped) end
|
||||
newline()
|
||||
end
|
||||
|
||||
if o.format_string_bottomwrapper ~= '' and overflow then
|
||||
draw_bottom_wrapper(wrapper_overrides)
|
||||
end
|
||||
|
||||
flush_buffer()
|
||||
draw()
|
||||
end
|
||||
|
||||
---@class ass
|
||||
return {
|
||||
update_ass = update_ass,
|
||||
highlight_entry = highlight_entry,
|
||||
draw = draw,
|
||||
remove = remove,
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local movement = require 'modules.navigation.directory-movement'
|
||||
local ass = require 'modules.ass'
|
||||
local cursor = require 'modules.navigation.cursor'
|
||||
|
||||
---@class controls
|
||||
local controls = {}
|
||||
|
||||
--opens the browser
|
||||
function controls.open()
|
||||
if not g.state.hidden then return end
|
||||
|
||||
for _,v in ipairs(g.state.keybinds) do
|
||||
mp.add_forced_key_binding(v[1], 'dynamic/'..v[2], v[3], v[4])
|
||||
end
|
||||
|
||||
if o.set_shared_script_properties then utils.shared_script_property_set('file_browser-open', 'yes') end ---@diagnostic disable-line deprecated
|
||||
if o.set_user_data then mp.set_property_bool('user-data/file_browser/open', true) end
|
||||
|
||||
if o.toggle_idlescreen then mp.commandv('script-message', 'osc-idlescreen', 'no', 'no_osd') end
|
||||
g.state.hidden = false
|
||||
if g.state.directory == nil then
|
||||
local path = mp.get_property('path')
|
||||
if path or o.default_to_working_directory then movement.goto_current_dir() else movement.goto_root() end
|
||||
return
|
||||
end
|
||||
|
||||
if not g.state.flag_update then ass.draw()
|
||||
else g.state.flag_update = false ; ass.update_ass() end
|
||||
end
|
||||
|
||||
--closes the list and sets the hidden flag
|
||||
function controls.close()
|
||||
if g.state.hidden then return end
|
||||
|
||||
for _,v in ipairs(g.state.keybinds) do
|
||||
mp.remove_key_binding('dynamic/'..v[2])
|
||||
end
|
||||
|
||||
if o.set_shared_script_properties then utils.shared_script_property_set("file_browser-open", "no") end ---@diagnostic disable-line deprecated
|
||||
if o.set_user_data then mp.set_property_bool('user-data/file_browser/open', false) end
|
||||
|
||||
if o.toggle_idlescreen then mp.commandv('script-message', 'osc-idlescreen', 'yes', 'no_osd') end
|
||||
g.state.hidden = true
|
||||
ass.remove()
|
||||
end
|
||||
|
||||
--toggles the list
|
||||
function controls.toggle()
|
||||
if g.state.hidden then controls.open()
|
||||
else controls.close() end
|
||||
end
|
||||
|
||||
--run when the escape key is used
|
||||
function controls.escape()
|
||||
--if multiple items are selection cancel the
|
||||
--selection instead of closing the browser
|
||||
if next(g.state.selection) or g.state.multiselect_start then
|
||||
g.state.selection = {}
|
||||
cursor.disable_select_mode()
|
||||
ass.update_ass()
|
||||
return
|
||||
end
|
||||
controls.close()
|
||||
end
|
||||
|
||||
---opens a specific directory
|
||||
---@param directory string
|
||||
---@param open_browser? boolean
|
||||
---@return thread|nil
|
||||
function controls.browse_directory(directory, open_browser)
|
||||
if not directory then return end
|
||||
if open_browser == nil then open_browser = true end
|
||||
|
||||
directory = mp.command_native({"expand-path", directory}, '') --[[@as string]]
|
||||
-- directory = join_path( mp.get_property("working-directory", ""), directory )
|
||||
|
||||
if directory ~= "" then directory = fb_utils.fix_path(directory, true) end
|
||||
msg.verbose('recieved directory from script message: '..directory)
|
||||
|
||||
directory = fb_utils.resolve_directory_mapping(directory)
|
||||
local co = movement.goto_directory(directory, nil, nil, {cache={use=false}})
|
||||
if open_browser then controls.open() end
|
||||
return co
|
||||
end
|
||||
|
||||
return controls
|
||||
@@ -1,3 +0,0 @@
|
||||
---@meta file-browser
|
||||
|
||||
return require 'modules.apis.fb'
|
||||
@@ -1,39 +0,0 @@
|
||||
---@meta _
|
||||
|
||||
---@class KeybindFlags
|
||||
---@field repeatable boolean?
|
||||
---@field scalable boolean?
|
||||
---@field complex boolean?
|
||||
|
||||
|
||||
---@class KeybindCommandTable
|
||||
|
||||
|
||||
---@class Keybind
|
||||
---@field key string
|
||||
---@field command KeybindCommand
|
||||
---@field api_version string?
|
||||
---
|
||||
---@field name string?
|
||||
---@field condition string?
|
||||
---@field flags KeybindFlags?
|
||||
---@field filter ('file'|'dir')?
|
||||
---@field parser string?
|
||||
---@field multiselect boolean?
|
||||
---@field multi-type ('repeat'|'concat')?
|
||||
---@field delay number?
|
||||
---@field concat-string string?
|
||||
---@field passthrough boolean?
|
||||
---
|
||||
---@field prev_key Keybind? The keybind that was previously set to the same key.
|
||||
---@field codes Set<string>? Any substituation codes used by the command table.
|
||||
---@field condition_codes Set<string>? Any substitution codes used by the condition string.
|
||||
---@field addon boolean? Whether the keybind was created by an addon.
|
||||
|
||||
|
||||
---@alias KeybindFunctionCallback async fun(keybind: Keybind, state: State, co: thread)
|
||||
|
||||
---@alias KeybindCommand KeybindFunctionCallback|KeybindCommandTable[]
|
||||
---@alias KeybindTuple [string,string,KeybindCommand,KeybindFlags?]
|
||||
---@alias KeybindTupleStrict [string,string,KeybindFunctionCallback,KeybindFlags?]
|
||||
---@alias KeybindList (Keybind|KeybindTuple)[]
|
||||
@@ -1,25 +0,0 @@
|
||||
---@meta _
|
||||
|
||||
---@alias List Item[]
|
||||
|
||||
---Represents an item returned by the parsers.
|
||||
---@class Item
|
||||
---@field type 'file'|'dir'
|
||||
---@field name string
|
||||
---@field label string?
|
||||
---@field path string?
|
||||
---@field ass string?
|
||||
---@field redirect boolean?
|
||||
---@field mpv_options string|{[string]: unknown}?
|
||||
|
||||
|
||||
---The Opts table returned by the parsers.
|
||||
---@class Opts
|
||||
---@field filtered boolean?
|
||||
---@field sorted boolean?
|
||||
---@field directory string?
|
||||
---@field directory_label string?
|
||||
---@field empty_text string?
|
||||
---@field selected_index number?
|
||||
---@field id string?
|
||||
---@field parser Parser?
|
||||
@@ -1,148 +0,0 @@
|
||||
---@meta mp
|
||||
|
||||
---@class mp
|
||||
local mp = {}
|
||||
|
||||
---@class AsyncReturn
|
||||
|
||||
---@class MPTimer
|
||||
---@field stop fun(self: MPTimer)
|
||||
---@field kill fun(self: MPTimer)
|
||||
---@field resume fun(self: MPTimer)
|
||||
---@field is_enabled fun(self: MPTimer): boolean
|
||||
---@field timeout number
|
||||
---@field oneshot boolean
|
||||
|
||||
---@class OSDOverlay
|
||||
---@field data string
|
||||
---@field res_x number
|
||||
---@field res_y number
|
||||
---@field z number
|
||||
---@field update fun(self:OSDOverlay)
|
||||
---@field remove fun(self: OSDOverlay)
|
||||
|
||||
---@class MPVSubprocessResult
|
||||
---@field status number
|
||||
---@field stdout string
|
||||
---@field stderr string
|
||||
---@field error_string ''|'killed'|'init'
|
||||
---@field killed_by_us boolean
|
||||
|
||||
---@param key string
|
||||
---@param name_or_fn string|function
|
||||
---@param fn? async fun()
|
||||
---@param flags? KeybindFlags
|
||||
function mp.add_key_binding(key, name_or_fn, fn, flags) end
|
||||
|
||||
---@param key string
|
||||
---@param name_or_fn string|function
|
||||
---@param fn? async fun()
|
||||
---@param flags? KeybindFlags
|
||||
function mp.add_forced_key_binding(key, name_or_fn, fn, flags) end
|
||||
|
||||
---@param seconds number
|
||||
---@param fn function
|
||||
---@param disabled? boolean
|
||||
---@return MPTimer
|
||||
function mp.add_timeout(seconds, fn, disabled) end
|
||||
|
||||
---@param format 'ass-events'
|
||||
---@return OSDOverlay
|
||||
function mp.create_osd_overlay(format) end
|
||||
|
||||
---@param ... string
|
||||
function mp.commandv(...) end
|
||||
|
||||
---@generic T
|
||||
---@param t table
|
||||
---@param def? T
|
||||
---@return unknown|T result
|
||||
---@return string? error
|
||||
---@overload fun(t: table): (unknown|nil, string?)
|
||||
function mp.command_native(t, def) end
|
||||
|
||||
---@nodiscard
|
||||
---@param t table
|
||||
---@param cb fun(success: boolean, result: unknown, error: string?)
|
||||
---@return AsyncReturn
|
||||
function mp.command_native_async(t, cb) end
|
||||
|
||||
---@param t AsyncReturn
|
||||
function mp.abort_async_command(t) end
|
||||
|
||||
---@generic T
|
||||
---@param name string
|
||||
---@param def? T
|
||||
---@return string|T
|
||||
---@overload fun(name: string): string|nil
|
||||
function mp.get_property(name, def) end
|
||||
|
||||
---@generic T
|
||||
---@param name string
|
||||
---@param def? T
|
||||
---@return boolean|T
|
||||
---@overload fun(name: string): boolean|nil
|
||||
function mp.get_property_bool(name, def) end
|
||||
|
||||
---@generic T
|
||||
---@param name string
|
||||
---@param def? T
|
||||
---@return number|T
|
||||
---@overload fun(name: string): number|nil
|
||||
function mp.get_property_number(name, def) end
|
||||
|
||||
---@generic T
|
||||
---@param name string
|
||||
---@param def? T
|
||||
---@return unknown|T
|
||||
---@overload fun(name: string): unknown|nil
|
||||
function mp.get_property_native(name, def) end
|
||||
|
||||
---@return string|nil
|
||||
function mp.get_script_directory() end
|
||||
|
||||
---@return string
|
||||
function mp.get_script_name() end
|
||||
|
||||
---@param name string
|
||||
---@param type 'native'|'bool'|'string'|'number'
|
||||
---@param fn fun(name: string, v: unknown)
|
||||
function mp.observe_property(name, type, fn) end
|
||||
|
||||
---@param name string
|
||||
---@param fn function
|
||||
---@return boolean
|
||||
function mp.register_event(name, fn) end
|
||||
|
||||
---@param name string
|
||||
---@param fn fun(...: string)
|
||||
function mp.register_script_message(name, fn) end
|
||||
|
||||
---@param name string
|
||||
function mp.remove_key_binding(name) end
|
||||
|
||||
---@param name string
|
||||
---@param value string
|
||||
---@return true? success # nil if error
|
||||
---@return string? err
|
||||
function mp.set_property(name, value) end
|
||||
|
||||
---@param name string
|
||||
---@param value boolean
|
||||
---@return true? success # nil if error
|
||||
---@return string? err
|
||||
function mp.set_property_bool(name, value) end
|
||||
|
||||
---@param name string
|
||||
---@param value number
|
||||
---@return true? success # nil if error
|
||||
---@return string? err
|
||||
function mp.set_property_number(name, value) end
|
||||
|
||||
---@param name string
|
||||
---@param value any
|
||||
---@return true? success # nil if error
|
||||
---@return string? err
|
||||
function mp.set_property_native(name, value) end
|
||||
|
||||
return mp
|
||||
@@ -1,21 +0,0 @@
|
||||
---@meta mp.input
|
||||
|
||||
---@class mp.input
|
||||
local input = {}
|
||||
|
||||
---@class InputGetOpts
|
||||
---@field prompt string?
|
||||
---@field default_text string?
|
||||
---@field id string?
|
||||
---@field submit (fun(text: string))?
|
||||
---@field opened (fun())?
|
||||
---@field edited (fun(text: string))?
|
||||
---@field complete (fun(text_before_cursor: string): string[], number)?
|
||||
---@field closed (fun(text: string))?
|
||||
|
||||
---@param options InputGetOpts
|
||||
function input.get(options) end
|
||||
|
||||
function input.terminate() end
|
||||
|
||||
return input
|
||||
@@ -1,32 +0,0 @@
|
||||
---@meta mp.msg
|
||||
|
||||
---@class mp.msg
|
||||
local msg = {}
|
||||
|
||||
---@param level 'fatal'|'error'|'warn'|'info'|'v'|'debug'|'trace'
|
||||
---@param ... any
|
||||
function msg.log(level, ...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.fatal(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.error(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.warn(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.info(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.verbose(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.debug(...) end
|
||||
|
||||
---@param ... any
|
||||
function msg.trace(...) end
|
||||
|
||||
|
||||
return msg
|
||||
@@ -1,11 +0,0 @@
|
||||
---@meta mp.options
|
||||
|
||||
---@class mp.options
|
||||
local options = {}
|
||||
|
||||
---@param t table<string,string|number|boolean>
|
||||
---@param identifier? string
|
||||
---@param on_update? fun(list: table<string,true|nil>)
|
||||
function options.read_options(t, identifier, on_update) end
|
||||
|
||||
return options
|
||||
@@ -1,43 +0,0 @@
|
||||
---@meta mp.utils
|
||||
|
||||
---@class mp.utils
|
||||
local utils = {}
|
||||
|
||||
---@param v string|boolean|number|table|nil
|
||||
---@return string? json # nil on error
|
||||
---@return string? err # error
|
||||
function utils.format_json(v) end
|
||||
|
||||
---@param p1 string
|
||||
---@param p2 string
|
||||
---@return string
|
||||
function utils.join_path(p1, p2) end
|
||||
|
||||
---@param str string
|
||||
---@param trail? boolean
|
||||
---@return (table|unknown[])? t
|
||||
---@return string? err # error
|
||||
---@return string trail # trailing characters
|
||||
function utils.parse_json(str, trail) end
|
||||
|
||||
---@param path string
|
||||
---@param filter ('files'|'dirs'|'normal'|'all')?
|
||||
---@return string[]? # nil on error
|
||||
---@return string? err # error
|
||||
function utils.readdir(path, filter) end
|
||||
|
||||
---@deprecated
|
||||
---@param name string
|
||||
---@param value string
|
||||
function utils.shared_script_property_set(name, value) end
|
||||
|
||||
---@param path string
|
||||
---@return string directory
|
||||
---@return string filename
|
||||
function utils.split_path(path) end
|
||||
|
||||
---@param v any
|
||||
---@return string
|
||||
function utils.to_string(v) end
|
||||
|
||||
return utils
|
||||
@@ -1,41 +0,0 @@
|
||||
---@meta _
|
||||
|
||||
---A ParserConfig object returned by addons
|
||||
---@class (partial) ParserConfig: ParserAPI
|
||||
---@field priority number?
|
||||
---@field api_version string The minimum API version the string requires.
|
||||
---@field version string? The minimum API version the string requires. @deprecated.
|
||||
---
|
||||
---@field can_parse (async fun(self: Parser, directory: string, parse_state: ParseState): boolean)?
|
||||
---@field parse (async fun(self: Parser, directory: string, parse_state: ParseState): List?, Opts?)?
|
||||
---@field setup fun(self: Parser)?
|
||||
---
|
||||
---@field name string?
|
||||
---@field keybind_name string?
|
||||
---@field keybinds KeybindList?
|
||||
|
||||
|
||||
---The parser object used by file-browser once the parsers have been loaded and initialised.
|
||||
---@class Parser: ParserAPI, ParserConfig
|
||||
---@field name string
|
||||
---@field priority number
|
||||
---@field api_version string
|
||||
---@field can_parse async fun(self: Parser, directory: string, parse_state: ParseState): boolean
|
||||
---@field parse async fun(self: Parser, directory: string, parse_state: ParseState): List?, Opts?
|
||||
|
||||
|
||||
---@alias ParseStateSource 'browser'|'loadlist'|'script-message'|'addon'|string
|
||||
---@alias ParseProperties table<string,any>
|
||||
|
||||
---The Parse State object passed to the can_parse and parse methods
|
||||
---@class ParseStateFields
|
||||
---@field source ParseStateSource
|
||||
---@field directory string
|
||||
---@field already_deferred boolean?
|
||||
---@field properties ParseProperties
|
||||
|
||||
---@class ParseState: ParseStateFields, ParseStateAPI
|
||||
|
||||
---@class ParseStateTemplate
|
||||
---@field source ParseStateSource?
|
||||
---@field properties ParseProperties?
|
||||
@@ -1,21 +0,0 @@
|
||||
---@meta _
|
||||
|
||||
---@class Set<T>: {[T]: boolean}
|
||||
|
||||
---@class (exact) State
|
||||
---@field list List
|
||||
---@field selected number
|
||||
---@field hidden boolean
|
||||
---@field flag_update boolean
|
||||
---@field keybinds KeybindTupleStrict[]?
|
||||
---
|
||||
---@field parser Parser?
|
||||
---@field directory string?
|
||||
---@field directory_label string?
|
||||
---@field prev_directory string
|
||||
---@field empty_text string
|
||||
---@field co thread?
|
||||
---
|
||||
---@field multiselect_start number?
|
||||
---@field initial_selection Set<number>?
|
||||
---@field selection Set<number>?
|
||||
@@ -1,28 +0,0 @@
|
||||
---@meta user-input-module
|
||||
|
||||
---@class user_input_module
|
||||
local user_input_module = {}
|
||||
|
||||
---@class UserInputOpts
|
||||
---@field id string?
|
||||
---@field source string?
|
||||
---@field request_text string?
|
||||
---@field default_input string?
|
||||
---@field cursor_pos number?
|
||||
---@field queueable boolean?
|
||||
---@field replace boolean?
|
||||
|
||||
---@class UserInputRequest
|
||||
---@field callback function?
|
||||
---@field passthrough_args any[]?
|
||||
---@field pending boolean
|
||||
---@field cancel fun(self: UserInputRequest)
|
||||
---@field update fun(self: UserInputRequest, opts: UserInputOpts)
|
||||
|
||||
---@param fn function
|
||||
---@param opts UserInputOpts
|
||||
---@param ... any passthrough arguments
|
||||
---@return UserInputRequest
|
||||
function user_input_module.get_user_input(fn, opts, ...) end
|
||||
|
||||
return user_input_module
|
||||
@@ -1,181 +0,0 @@
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
------------------------------------------Variable Setup------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
|
||||
local mp = require 'mp'
|
||||
local o = require 'modules.options'
|
||||
|
||||
---@class globals
|
||||
local globals = {}
|
||||
|
||||
--sets the version for the file-browser API
|
||||
globals.API_VERSION = "1.9.0"
|
||||
|
||||
---gets the current platform (in mpv v0.36+)
|
||||
---in earlier versions it is set to `windows`, `darwin` or `other`
|
||||
---@type 'windows'|'darwin'|'linux'|'android'|'freebsd'|'other'|string|nil
|
||||
globals.PLATFORM = mp.get_property_native('platform')
|
||||
if not globals.PLATFORM then
|
||||
local _ = {}
|
||||
if mp.get_property_native('options/vo-mmcss-profile', _) ~= _ then
|
||||
globals.PLATFORM = 'windows'
|
||||
elseif mp.get_property_native('options/macos-force-dedicated-gpu', _) ~= _ then
|
||||
globals.PLATFORM = 'darwin'
|
||||
end
|
||||
return 'other'
|
||||
end
|
||||
|
||||
--the osd_overlay API was not added until v0.31. The expand-path command was not added until 0.30
|
||||
assert(mp.create_osd_overlay, "Script requires minimum mpv version 0.33")
|
||||
|
||||
globals.ass = mp.create_osd_overlay("ass-events")
|
||||
globals.ass.res_y = 720 / o.scaling_factor_base
|
||||
|
||||
local BASE_FONT_SIZE = 25
|
||||
|
||||
--force file-browser to use a specific text alignment (default: top-left)
|
||||
--uses ass tag alignment numbers: https://aegi.vmoe.info/docs/3.0/ASS_Tags/#index23h3
|
||||
globals.ASS_ALIGNMENT_MATRIX = {
|
||||
top = {left = 7, center = 8, right = 9},
|
||||
center = {left = 4, center = 5, right = 6},
|
||||
bottom = {left = 1, center = 2, right = 3},
|
||||
}
|
||||
|
||||
globals.ALIGN_X = o.align_x == 'auto' and mp.get_property('osd-align-x', 'left') or o.align_x
|
||||
globals.ALIGN_Y = o.align_y == 'auto' and mp.get_property('osd-align-y', 'top') or o.align_y
|
||||
|
||||
globals.style = {
|
||||
global = ([[{\an%d}]]):format(globals.ASS_ALIGNMENT_MATRIX[globals.ALIGN_Y][globals.ALIGN_X]),
|
||||
|
||||
-- full line styles
|
||||
header = ([[{\r\q2\b%s\fs%d\fn%s\c&H%s&}]]):format((o.font_bold_header and "1" or "0"), o.scaling_factor_header*BASE_FONT_SIZE, o.font_name_header, o.font_colour_header),
|
||||
body = ([[{\r\q2\fs%d\fn%s\c&H%s&}]]):format(BASE_FONT_SIZE, o.font_name_body, o.font_colour_body),
|
||||
footer_header = ([[{\r\q2\fs%d\fn%s\c&H%s&}]]):format(o.scaling_factor_wrappers*BASE_FONT_SIZE, o.font_name_wrappers, o.font_colour_wrappers),
|
||||
|
||||
--small section styles (for colours)
|
||||
multiselect = ([[{\c&H%s&}]]):format(o.font_colour_multiselect),
|
||||
selected = ([[{\c&H%s&}]]):format(o.font_colour_selected),
|
||||
playing = ([[{\c&H%s&}]]):format(o.font_colour_playing),
|
||||
playing_selected = ([[{\c&H%s&}]]):format(o.font_colour_playing_multiselected),
|
||||
warning = ([[{\c&H%s&}]]):format(o.font_colour_escape_chars),
|
||||
|
||||
--icon styles
|
||||
indent = ([[{\alpha&H%s}]]):format('ff'),
|
||||
cursor = ([[{\fn%s\c&H%s&}]]):format(o.font_name_cursor, o.font_colour_cursor),
|
||||
cursor_select = ([[{\fn%s\c&H%s&}]]):format(o.font_name_cursor, o.font_colour_multiselect),
|
||||
cursor_deselect = ([[{\fn%s\c&H%s&}]]):format(o.font_name_cursor, o.font_colour_selected),
|
||||
folder = ([[{\fn%s}]]):format(o.font_name_folder),
|
||||
selection_marker = ([[{\alpha&H%s}]]):format(o.font_opacity_selection_marker),
|
||||
}
|
||||
|
||||
---@type State
|
||||
globals.state = {
|
||||
list = {},
|
||||
selected = 1,
|
||||
hidden = true,
|
||||
flag_update = false,
|
||||
keybinds = nil,
|
||||
|
||||
parser = nil,
|
||||
directory = nil,
|
||||
directory_label = nil,
|
||||
prev_directory = '',
|
||||
empty_text = 'Empty Directory',
|
||||
co = nil,
|
||||
|
||||
multiselect_start = nil,
|
||||
initial_selection = nil,
|
||||
selection = {}
|
||||
}
|
||||
|
||||
---@class ParserRef
|
||||
---@field id string
|
||||
---@field index number?
|
||||
|
||||
---@type table<number,Parser>|table<string,Parser>|table<Parser,ParserRef>>
|
||||
--the parser table actually contains 3 entries for each parser
|
||||
--a numeric entry which represents the priority of the parsers and has the parser object as the value
|
||||
--a string entry representing the id of each parser and with the parser object as the value
|
||||
--and a table entry with the parser itself as the key and a table value in the form { id = %s, index = %d }
|
||||
globals.parsers = {}
|
||||
|
||||
--this table contains the parse_state tables for every parse operation indexed with the coroutine used for the parse
|
||||
--this table has weakly referenced keys, meaning that once the coroutine for a parse is no-longer used by anything that
|
||||
--field in the table will be removed by the garbage collector
|
||||
---@type table<thread,ParseState>
|
||||
globals.parse_states = setmetatable({}, { __mode = "k"})
|
||||
|
||||
---@type Set<string>
|
||||
globals.extensions = {}
|
||||
|
||||
---@type Set<string>
|
||||
globals.sub_extensions = {}
|
||||
|
||||
---@type Set<string>
|
||||
globals.audio_extensions = {}
|
||||
|
||||
---@type Set<string>
|
||||
globals.parseable_extensions = {}
|
||||
|
||||
---This table contains mappings to convert external directories to cannonical
|
||||
--locations within the file-browser file tree. The keys of the table are Lua
|
||||
--patterns used to evaluate external directory paths. The value is the path
|
||||
--that should replace the part of the path than matched the pattern.
|
||||
--These mappings should only applied at the edges where external paths are
|
||||
--ingested by file-browser.
|
||||
---@type table<string,string>
|
||||
globals.directory_mappings = {}
|
||||
|
||||
---@class CurrentFile
|
||||
---@field directory string?
|
||||
---@field name string?
|
||||
---@field path string?
|
||||
---@field original_path string?
|
||||
globals.current_file = {
|
||||
directory = nil,
|
||||
name = nil,
|
||||
path = nil,
|
||||
original_path = nil,
|
||||
}
|
||||
|
||||
---@type List
|
||||
globals.root = {}
|
||||
|
||||
---@class (strict) History
|
||||
---@field list string[]
|
||||
---@field size number
|
||||
---@field position number
|
||||
globals.history = {
|
||||
list = {},
|
||||
size = 0,
|
||||
position = 0,
|
||||
}
|
||||
|
||||
---@class (strict) DirectoryStack
|
||||
---@field stack string[]
|
||||
---@field position number
|
||||
globals.directory_stack = {
|
||||
stack = {},
|
||||
position = 0,
|
||||
}
|
||||
|
||||
|
||||
--default list of compatible file extensions
|
||||
--adding an item to this list is a valid request on github
|
||||
globals.compatible_file_extensions = {
|
||||
"264","265","3g2","3ga","3ga2","3gp","3gp2","3gpp","3iv","a52","aac","adt","adts","ahn","aif","aifc","aiff","amr","ape","asf","au","avc","avi","awb","ay",
|
||||
"bmp","cue","divx","dts","dtshd","dts-hd","dv","dvr","dvr-ms","eac3","evo","evob","f4a","flac","flc","fli","flic","flv","gbs","gif","gxf","gym",
|
||||
"h264","h265","hdmov","hdv","hes","hevc","jpeg","jpg","kss","lpcm","m1a","m1v","m2a","m2t","m2ts","m2v","m3u","m3u8","m4a","m4v","mk3d","mka","mkv",
|
||||
"mlp","mod","mov","mp1","mp2","mp2v","mp3","mp4","mp4v","mp4v","mpa","mpe","mpeg","mpeg2","mpeg4","mpg","mpg4","mpv","mpv2","mts","mtv","mxf","nsf",
|
||||
"nsfe","nsv","nut","oga","ogg","ogm","ogv","ogx","opus","pcm","pls","png","qt","ra","ram","rm","rmvb","sap","snd","spc","spx","svg","thd","thd+ac3",
|
||||
"tif","tiff","tod","trp","truehd","true-hd","ts","tsa","tsv","tta","tts","vfw","vgm","vgz","vob","vro","wav","weba","webm","webp","wm","wma","wmv","wtv",
|
||||
"wv","x264","x265","xvid","y4m","yuv"
|
||||
}
|
||||
|
||||
---@class BrowserAbortError
|
||||
globals.ABORT_ERROR = {
|
||||
msg = "browser is no longer waiting for list - aborting parse"
|
||||
}
|
||||
|
||||
return globals
|
||||
@@ -1,354 +0,0 @@
|
||||
------------------------------------------------------------------------------------------
|
||||
----------------------------------Keybind Implementation----------------------------------
|
||||
------------------------------------------------------------------------------------------
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local addons = require 'modules.addons'
|
||||
local playlist = require 'modules.playlist'
|
||||
local controls = require 'modules.controls'
|
||||
local movement = require 'modules.navigation.directory-movement'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
local cursor = require 'modules.navigation.cursor'
|
||||
|
||||
g.state.keybinds = {
|
||||
{'ENTER', 'play', function() playlist.add_files('replace', false) end},
|
||||
{'Shift+ENTER', 'play_append', function() playlist.add_files('append-play', false) end},
|
||||
{'Alt+ENTER', 'play_autoload', function() playlist.add_files('replace', true) end},
|
||||
{'ESC', 'close', controls.escape},
|
||||
{'RIGHT', 'down_dir', movement.down_dir},
|
||||
{'LEFT', 'up_dir', movement.up_dir},
|
||||
{'Alt+RIGHT', 'history_forward', movement.forwards_history},
|
||||
{'Alt+LEFT', 'history_back', movement.back_history},
|
||||
{'DOWN', 'scroll_down', function() cursor.scroll(1, o.wrap) end, {repeatable = true}},
|
||||
{'UP', 'scroll_up', function() cursor.scroll(-1, o.wrap) end, {repeatable = true}},
|
||||
{'PGDWN', 'page_down', function() cursor.scroll(o.num_entries) end, {repeatable = true}},
|
||||
{'PGUP', 'page_up', function() cursor.scroll(-o.num_entries) end, {repeatable = true}},
|
||||
{'Shift+PGDWN', 'list_bottom', function() cursor.scroll(math.huge) end},
|
||||
{'Shift+PGUP', 'list_top', function() cursor.scroll(-math.huge) end},
|
||||
{'HOME', 'goto_current', movement.goto_current_dir},
|
||||
{'Shift+HOME', 'goto_root', movement.goto_root},
|
||||
{'Ctrl+r', 'reload', function() scanning.rescan() end},
|
||||
{'s', 'select_mode', cursor.toggle_select_mode},
|
||||
{'S', 'select_item', cursor.toggle_selection},
|
||||
{'Ctrl+a', 'select_all', cursor.select_all}
|
||||
}
|
||||
|
||||
---a map of key-keybinds - only saves the latest keybind if multiple have the same key code
|
||||
---@type KeybindList
|
||||
local top_level_keys = {}
|
||||
|
||||
---Format the item string for either single or multiple items.
|
||||
---@param base_code_fn Replacer
|
||||
---@param items Item[]
|
||||
---@param state State
|
||||
---@param cmd Keybind
|
||||
---@param quoted? boolean
|
||||
---@return string|nil
|
||||
local function create_item_string(base_code_fn, items, state, cmd, quoted)
|
||||
if not items[1] then return end
|
||||
local func = quoted and function(...) return ("%q"):format(base_code_fn(...)) end or base_code_fn
|
||||
|
||||
local out = {}
|
||||
for _, item in ipairs(items) do
|
||||
table.insert(out, func(item, state))
|
||||
end
|
||||
|
||||
return table.concat(out, cmd['concat-string'] or ' ')
|
||||
end
|
||||
|
||||
local KEYBIND_CODE_PATTERN = fb_utils.get_code_pattern(fb_utils.code_fns)
|
||||
local item_specific_codes = 'fnij'
|
||||
|
||||
---Replaces codes in the given string using the replacers.
|
||||
---@param str string
|
||||
---@param cmd Keybind
|
||||
---@param items Item[]
|
||||
---@param state State
|
||||
---@return string
|
||||
local function substitute_codes(str, cmd, items, state)
|
||||
---@type ReplacerTable
|
||||
local overrides = {}
|
||||
|
||||
for code in item_specific_codes:gmatch('.') do
|
||||
overrides[code] = function(_,s) return create_item_string(fb_utils.code_fns[code], items, s, cmd) end
|
||||
overrides[code:upper()] = function(_,s) return create_item_string(fb_utils.code_fns[code], items, s, cmd, true) end
|
||||
end
|
||||
|
||||
return fb_utils.substitute_codes(str, overrides, items[1], state)
|
||||
end
|
||||
|
||||
---Iterates through the command table and substitutes special
|
||||
---character codes for the correct strings used for custom functions.
|
||||
---@param cmd Keybind
|
||||
---@param items Item[]
|
||||
---@param state State
|
||||
---@return KeybindCommand
|
||||
local function format_command_table(cmd, items, state)
|
||||
local command = cmd.command
|
||||
if type(command) == 'function' then return command end
|
||||
---@type string[][]
|
||||
local copy = {}
|
||||
for i = 1, #command do
|
||||
---@type string[]
|
||||
copy[i] = {}
|
||||
|
||||
for j = 1, #command[i] do
|
||||
copy[i][j] = substitute_codes(cmd.command[i][j], cmd, items, state)
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
---Runs all of the commands in the command table.
|
||||
---@param cmd Keybind key.command must be an array of command tables compatible with mp.command_native
|
||||
---@param items Item[] must be an array of multiple items (when multi-type ~= concat the array will be 1 long).
|
||||
---@param state State
|
||||
local function run_custom_command(cmd, items, state)
|
||||
local custom_cmds = cmd.codes and format_command_table(cmd, items, state) or cmd.command
|
||||
if type(custom_cmds) == 'function' then
|
||||
error(('attempting to run a function keybind as a command table keybind\n%s'):format(utils.to_string(cmd)))
|
||||
end
|
||||
|
||||
for _, custom_cmd in ipairs(custom_cmds) do
|
||||
msg.debug("running command:", utils.to_string(custom_cmd))
|
||||
mp.command_native(custom_cmd)
|
||||
end
|
||||
end
|
||||
|
||||
---returns true if the given code set has item specific codes (%f, %i, etc)
|
||||
---@param codes Set<string>
|
||||
---@return boolean
|
||||
local function has_item_codes(codes)
|
||||
for code in pairs(codes) do
|
||||
if item_specific_codes:find(code:lower(), 1, true) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Runs one of the custom commands.
|
||||
---@async
|
||||
---@param cmd Keybind
|
||||
---@param state State
|
||||
---@param co thread
|
||||
---@return boolean|nil
|
||||
local function run_custom_keybind(cmd, state, co)
|
||||
--evaluates a condition and passes through the correct values
|
||||
local function evaluate_condition(condition, items)
|
||||
local cond = substitute_codes(condition, cmd, items, state)
|
||||
return fb_utils.evaluate_string('return '..cond) == true
|
||||
end
|
||||
|
||||
-- evaluates the string condition to decide if the keybind should be run
|
||||
---@type boolean
|
||||
local do_item_condition
|
||||
if cmd.condition then
|
||||
if has_item_codes(cmd.condition_codes) then
|
||||
do_item_condition = true
|
||||
elseif not evaluate_condition(cmd.condition, {}) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if cmd.parser then
|
||||
local parser_str = ' '..cmd.parser..' '
|
||||
if not parser_str:find( '%W'..(state.parser.keybind_name or state.parser.name)..'%W' ) then return false end
|
||||
end
|
||||
|
||||
--these are for the default keybinds, or from addons which use direct functions
|
||||
if type(cmd.command) == 'function' then return cmd.command(cmd, cmd.addon and fb_utils.copy_table(state) or state, co) end
|
||||
|
||||
--the function terminates here if we are running the command on a single item
|
||||
if not (cmd.multiselect and next(state.selection)) then
|
||||
if cmd.filter then
|
||||
if not state.list[state.selected] then return false end
|
||||
if state.list[state.selected].type ~= cmd.filter then return false end
|
||||
end
|
||||
|
||||
if cmd.codes then
|
||||
--if the directory is empty, and this command needs to work on an item, then abort and fallback to the next command
|
||||
if not state.list[state.selected] and has_item_codes(cmd.codes) then return false end
|
||||
end
|
||||
|
||||
if do_item_condition and not evaluate_condition(cmd.condition, { state.list[state.selected] }) then
|
||||
return false
|
||||
end
|
||||
run_custom_command(cmd, { state.list[state.selected] }, state)
|
||||
return true
|
||||
end
|
||||
|
||||
--runs the command on all multi-selected items
|
||||
local selection = fb_utils.sort_keys(state.selection, function(item)
|
||||
if do_item_condition and not evaluate_condition(cmd.condition, { item }) then return false end
|
||||
return not cmd.filter or item.type == cmd.filter
|
||||
end)
|
||||
if not next(selection) then return false end
|
||||
|
||||
if cmd["multi-type"] == "concat" then
|
||||
run_custom_command(cmd, selection, state)
|
||||
|
||||
elseif cmd["multi-type"] == "repeat" or cmd["multi-type"] == nil then
|
||||
for i,_ in ipairs(selection) do
|
||||
run_custom_command(cmd, {selection[i]}, state)
|
||||
|
||||
if cmd.delay then
|
||||
mp.add_timeout(cmd.delay, function() fb_utils.coroutine.resume_err(co) end)
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--we passthrough by default if the command is not run on every selected item
|
||||
if cmd.passthrough ~= nil then return end
|
||||
|
||||
local num_selection = 0
|
||||
for _ in pairs(state.selection) do num_selection = num_selection+1 end
|
||||
return #selection == num_selection
|
||||
end
|
||||
|
||||
---Recursively runs the keybind functions, passing down through the chain
|
||||
---of keybinds with the same key value.
|
||||
---@async
|
||||
---@param keybind Keybind
|
||||
---@param state State
|
||||
---@param co thread
|
||||
local function run_keybind_recursive(keybind, state, co)
|
||||
msg.trace("Attempting custom command:", utils.to_string(keybind))
|
||||
|
||||
if keybind.passthrough ~= nil then
|
||||
run_custom_keybind(keybind, state, co)
|
||||
if keybind.passthrough == true and keybind.prev_key then
|
||||
run_keybind_recursive(keybind.prev_key, state, co)
|
||||
end
|
||||
else
|
||||
if run_custom_keybind(keybind, state, co) == false and keybind.prev_key then
|
||||
run_keybind_recursive(keybind.prev_key, state, co)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---A wrapper to run a custom keybind as a lua coroutine.
|
||||
---@param key Keybind
|
||||
local function run_keybind_coroutine(key)
|
||||
msg.debug("Received custom keybind "..key.key)
|
||||
local co = coroutine.create(run_keybind_recursive)
|
||||
|
||||
local state_copy = {
|
||||
directory = g.state.directory,
|
||||
directory_label = g.state.directory_label,
|
||||
list = g.state.list, --the list should remain unchanged once it has been saved to the global state, new directories get new tables
|
||||
selected = g.state.selected,
|
||||
selection = fb_utils.copy_table(g.state.selection),
|
||||
parser = g.state.parser,
|
||||
}
|
||||
local success, err = coroutine.resume(co, key, state_copy, co)
|
||||
if not success then
|
||||
msg.error("error running keybind:", utils.to_string(key))
|
||||
fb_utils.traceback(err, co)
|
||||
end
|
||||
end
|
||||
|
||||
---Scans the given command table to identify if they contain any custom keybind codes.
|
||||
---@param command_table KeybindCommand
|
||||
---@param codes Set<string>
|
||||
---@return Set<string>
|
||||
local function scan_for_codes(command_table, codes)
|
||||
if type(command_table) ~= "table" then return codes end
|
||||
for _, value in pairs(command_table) do
|
||||
local type = type(value)
|
||||
if type == "table" then
|
||||
scan_for_codes(value, codes)
|
||||
elseif type == "string" then
|
||||
for code in value:gmatch(KEYBIND_CODE_PATTERN) do
|
||||
codes[code] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return codes
|
||||
end
|
||||
|
||||
---Inserting the custom keybind into the keybind array for declaration when file-browser is opened.
|
||||
---Custom keybinds with matching names will overwrite eachother.
|
||||
---@param keybind Keybind
|
||||
local function insert_custom_keybind(keybind)
|
||||
-- api checking for the keybinds is optional, so set to a valid version if it does not exist
|
||||
keybind.api_version = keybind.api_version or '1.0.0'
|
||||
if not addons.check_api_version(keybind, 'keybind '..keybind.name) then return end
|
||||
|
||||
local command = keybind.command
|
||||
|
||||
--we'll always save the keybinds as either an array of command arrays or a function
|
||||
if type(command) == "table" and type(command[1]) ~= "table" then
|
||||
keybind.command = {command}
|
||||
end
|
||||
|
||||
keybind.codes = scan_for_codes(keybind.command, {})
|
||||
if not next(keybind.codes) then keybind.codes = nil end
|
||||
keybind.prev_key = top_level_keys[keybind.key]
|
||||
|
||||
if keybind.condition then
|
||||
keybind.condition_codes = {}
|
||||
for code in string.gmatch(keybind.condition, KEYBIND_CODE_PATTERN) do keybind.condition_codes[code] = true end
|
||||
end
|
||||
|
||||
table.insert(g.state.keybinds, {keybind.key, keybind.name, function() run_keybind_coroutine(keybind) end, keybind.flags or {}})
|
||||
top_level_keys[keybind.key] = keybind
|
||||
end
|
||||
|
||||
---Loading the custom keybinds.
|
||||
---Can either load keybinds from the config file, from addons, or from both.
|
||||
local function setup_keybinds()
|
||||
--this is to make the default keybinds compatible with passthrough from custom keybinds
|
||||
for _, keybind in ipairs(g.state.keybinds) do
|
||||
top_level_keys[keybind[1]] = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] }
|
||||
end
|
||||
|
||||
--this loads keybinds from addons
|
||||
for i = #g.parsers, 1, -1 do
|
||||
local parser = g.parsers[i]
|
||||
if parser.keybinds then
|
||||
for i, keybind in ipairs(parser.keybinds) do
|
||||
--if addons use the native array command format, then we need to convert them over to the custom command format
|
||||
if not keybind.key then keybind = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] }
|
||||
else keybind = fb_utils.copy_table(keybind) end
|
||||
|
||||
keybind.name = g.parsers[parser].id.."/"..(keybind.name or tostring(i))
|
||||
keybind.addon = true
|
||||
insert_custom_keybind(keybind)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--loads custom keybinds from file-browser-keybinds.json
|
||||
if o.custom_keybinds then
|
||||
local path = mp.command_native({"expand-path", o.custom_keybinds_file}) --[[@as string]]
|
||||
local custom_keybinds, err = io.open( path )
|
||||
if not custom_keybinds then
|
||||
msg.debug(err)
|
||||
msg.verbose('could not read custom keybind file', path)
|
||||
return
|
||||
end
|
||||
|
||||
local json = custom_keybinds:read("*a")
|
||||
custom_keybinds:close()
|
||||
|
||||
json = utils.parse_json(json)
|
||||
if not json then return error("invalid json syntax for "..path) end
|
||||
|
||||
for i, keybind in ipairs(json --[[@as KeybindList]]) do
|
||||
keybind.name = "custom/"..(keybind.name or tostring(i))
|
||||
insert_custom_keybind(keybind)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class keybinds
|
||||
return {
|
||||
setup_keybinds = setup_keybinds,
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
--------------------------------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
|
||||
@@ -1,209 +0,0 @@
|
||||
|
||||
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
|
||||
@@ -1,210 +0,0 @@
|
||||
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
|
||||
@@ -1,48 +0,0 @@
|
||||
local g = require 'modules.globals'
|
||||
local directory_movement = require 'modules.navigation.directory-movement'
|
||||
local fb = require 'modules.apis.fb'
|
||||
local fb_utils = require 'modules.utils'
|
||||
local ass = require 'modules.ass'
|
||||
|
||||
---@class observers
|
||||
local observers ={}
|
||||
|
||||
---saves the directory and name of the currently playing file
|
||||
---@param _ string
|
||||
---@param filepath string
|
||||
function observers.current_directory(_, filepath)
|
||||
directory_movement.set_current_file(filepath)
|
||||
end
|
||||
|
||||
---@param _ string
|
||||
---@param device string
|
||||
function observers.dvd_device(_, device)
|
||||
if not device or device == "" then device = '/dev/dvd' end
|
||||
fb.register_directory_mapping(fb_utils.absolute_path(device), '^dvd://.*', true)
|
||||
end
|
||||
|
||||
---@param _ string
|
||||
---@param device string
|
||||
function observers.bd_device(_, device)
|
||||
if not device or device == '' then device = '/dev/bd' end
|
||||
fb.register_directory_mapping(fb_utils.absolute_path(device), '^bd://.*', true)
|
||||
end
|
||||
|
||||
---@param _ string
|
||||
---@param device string
|
||||
function observers.cd_device(_, device)
|
||||
if not device or device == '' then device = '/dev/cdrom' end
|
||||
fb.register_directory_mapping(fb_utils.absolute_path(device), '^cdda://.*', true)
|
||||
end
|
||||
|
||||
---@param property string
|
||||
---@param alignment string
|
||||
function observers.osd_align(property, alignment)
|
||||
if property == 'osd-align-x' then g.ALIGN_X = alignment
|
||||
elseif property == 'osd-align-y' then g.ALIGN_Y = alignment end
|
||||
|
||||
g.style.global = ([[{\an%d}]]):format(g.ASS_ALIGNMENT_MATRIX[g.ALIGN_Y][g.ALIGN_X])
|
||||
ass.update_ass()
|
||||
end
|
||||
|
||||
return observers
|
||||
@@ -1,193 +0,0 @@
|
||||
local utils = require 'mp.utils'
|
||||
local opt = require 'mp.options'
|
||||
|
||||
---@class options
|
||||
local o = {
|
||||
--root directories
|
||||
root = "~/",
|
||||
|
||||
--automatically detect windows drives and adds them to the root.
|
||||
auto_detect_windows_drives = true,
|
||||
|
||||
--characters to use as separators
|
||||
root_separators = ",",
|
||||
|
||||
--number of entries to show on the screen at once
|
||||
num_entries = 20,
|
||||
|
||||
--number of directories to keep in the history
|
||||
history_size = 100,
|
||||
|
||||
--wrap the cursor around the top and bottom of the list
|
||||
wrap = false,
|
||||
|
||||
--only show files compatible with mpv
|
||||
filter_files = true,
|
||||
|
||||
--recurses directories concurrently when appending items to the playlist
|
||||
concurrent_recursion = true,
|
||||
|
||||
--maximum number of recursions that can run concurrently
|
||||
max_concurrency = 16,
|
||||
|
||||
--enable custom keybinds
|
||||
custom_keybinds = true,
|
||||
custom_keybinds_file = "~~/script-opts/file-browser-keybinds.json",
|
||||
|
||||
--blacklist compatible files, it's recommended to use this rather than to edit the
|
||||
--compatible list directly. A comma separated list of extensions without spaces
|
||||
extension_blacklist = "",
|
||||
|
||||
--add extra file extensions
|
||||
extension_whitelist = "",
|
||||
|
||||
--files with these extensions will be added as additional audio tracks for the current file instead of appended to the playlist
|
||||
audio_extensions = "mka,dts,dtshd,dts-hd,truehd,true-hd",
|
||||
|
||||
--files with these extensions will be added as additional subtitle tracks instead of appended to the playlist
|
||||
subtitle_extensions = "etf,etf8,utf-8,idx,sub,srt,rt,ssa,ass,mks,vtt,sup,scc,smi,lrc,pgs",
|
||||
|
||||
--filter dot directories like .config
|
||||
--most useful on linux systems
|
||||
---@type 'auto'|'yes'|'no'
|
||||
filter_dot_dirs = 'auto',
|
||||
---@type 'auto'|'yes'|'no'
|
||||
filter_dot_files = 'auto',
|
||||
|
||||
--substitute forward slashes for backslashes when appending a local file to the playlist
|
||||
--potentially useful on windows systems
|
||||
substitute_backslash = false,
|
||||
|
||||
--interpret backslashes `\` in paths as forward slashes `/`
|
||||
--this is useful on Windows, which natively uses backslashes.
|
||||
--As backslashes are valid filename characters in Unix systems this could
|
||||
--cause mangled paths, though such filenames are rare.
|
||||
--Use `yes` and `no` to enable/disable. `auto` tries to use the mpv `platform`
|
||||
--property (mpv v0.36+) to decide. If the property is unavailable it defaults to `yes`.
|
||||
---@type 'auto'|'yes'|'no'
|
||||
normalise_backslash = 'auto',
|
||||
|
||||
--a directory cache to improve directory reading time,
|
||||
--enable if it takes a long time to load directories.
|
||||
--may cause 'ghost' files to be shown that no-longer exist or
|
||||
--fail to show files that have recently been created.
|
||||
cache = false,
|
||||
|
||||
--this option reverses the behaviour of the alt+ENTER keybind
|
||||
--when disabled the keybind is required to enable autoload for the file
|
||||
--when enabled the keybind disables autoload for the file
|
||||
autoload = false,
|
||||
|
||||
--if autoload is triggered by selecting the currently playing file, then
|
||||
--the current file will have it's watch-later config saved before being closed
|
||||
--essentially the current file will not be restarted
|
||||
autoload_save_current = true,
|
||||
|
||||
--when opening the browser in idle mode prefer the current working directory over the root
|
||||
--note that the working directory is set as the 'current' directory regardless, so `home` will
|
||||
--move the browser there even if this option is set to false
|
||||
default_to_working_directory = false,
|
||||
|
||||
--When opening the browser prefer the directory last opened by a previous mpv instance of file-browser.
|
||||
--Overrides the `default_to_working_directory` option.
|
||||
--Requires `save_last_opened_directory` to be true.
|
||||
--Uses the internal `last-opened-directory` addon.
|
||||
default_to_last_opened_directory = false,
|
||||
|
||||
--Whether to save the last opened directory and the file to save this value in.
|
||||
save_last_opened_directory = false,
|
||||
last_opened_directory_file = '~~state/file_browser-last_opened_directory',
|
||||
|
||||
--when moving up a directory do not stop on empty protocol schemes like `ftp://`
|
||||
--e.g. moving up from `ftp://localhost/` will move straight to the root instead of `ftp://`
|
||||
skip_protocol_schemes = true,
|
||||
|
||||
--move the cursor to the currently playing item (if available) when the playing file changes
|
||||
cursor_follows_playing_item = false,
|
||||
|
||||
--Replace the user's home directory with `~/` in the header.
|
||||
--Uses the internal home-label addon.
|
||||
home_label = true,
|
||||
|
||||
--map optical device paths to their respective file paths,
|
||||
--e.g. mapping bd:// to the value of the bluray-device property
|
||||
map_bd_device = true,
|
||||
map_dvd_device = true,
|
||||
map_cdda_device = true,
|
||||
|
||||
--allows custom icons be set for the folder and cursor
|
||||
--the `\h` character is a hard space to add padding between the symbol and the text
|
||||
folder_icon = [[{\p1}m 6.52 0 l 1.63 0 b 0.73 0 0.01 0.73 0.01 1.63 l 0 11.41 b 0 12.32 0.73 13.05 1.63 13.05 l 14.68 13.05 b 15.58 13.05 16.31 12.32 16.31 11.41 l 16.31 3.26 b 16.31 2.36 15.58 1.63 14.68 1.63 l 8.15 1.63{\p0}\h]],
|
||||
cursor_icon = [[{\p1}m 14.11 6.86 l 0.34 0.02 b 0.25 -0.02 0.13 -0 0.06 0.08 b -0.01 0.16 -0.02 0.28 0.04 0.36 l 3.38 5.55 l 3.38 5.55 3.67 6.15 3.81 6.79 3.79 7.45 3.61 8.08 3.39 8.5l 0.04 13.77 b -0.02 13.86 -0.01 13.98 0.06 14.06 b 0.11 14.11 0.17 14.13 0.24 14.13 b 0.27 14.13 0.31 14.13 0.34 14.11 l 14.11 7.28 b 14.2 7.24 14.25 7.16 14.25 7.07 b 14.25 6.98 14.2 6.9 14.11 6.86{\p0}\h]],
|
||||
cursor_icon_flipped = [[{\p1}m 0.13 6.86 l 13.9 0.02 b 14 -0.02 14.11 -0 14.19 0.08 b 14.26 0.16 14.27 0.28 14.21 0.36 l 10.87 5.55 l 10.87 5.55 10.44 6.79 10.64 8.08 10.86 8.5l 14.21 13.77 b 14.27 13.86 14.26 13.98 14.19 14.06 b 14.14 14.11 14.07 14.13 14.01 14.13 b 13.97 14.13 13.94 14.13 13.9 14.11 l 0.13 7.28 b 0.05 7.24 0 7.16 0 7.07 b 0 6.98 0.05 6.9 0.13 6.86{\p0}\h]],
|
||||
|
||||
--enable addons
|
||||
addons = true,
|
||||
addon_directory = "~~/script-modules/file-browser-addons",
|
||||
|
||||
--Enables the internal `ls` addon that parses directories using the `ls` commandline tool.
|
||||
--Allows directory parsing to run concurrently, which prevents the browser from locking up.
|
||||
--Automatically disables itself on Windows systems.
|
||||
ls_parser = true,
|
||||
|
||||
--Enables the internal `windir` addon that parses directories using the `dir` command in cmd.exe.
|
||||
--Allows directory parsing to run concurrently, which prevents the browser from locking up.
|
||||
--Automatically disables itself on non-Windows systems.
|
||||
windir_parser = true,
|
||||
|
||||
--directory to load external modules - currently just user-input-module
|
||||
module_directory = "~~/script-modules",
|
||||
|
||||
--turn the OSC idle screen off and on when opening and closing the browser
|
||||
toggle_idlescreen = false,
|
||||
|
||||
--Set the current open status of the browser in the `file_browser/open` field of the `user-data` property.
|
||||
--This property is only available in mpv v0.36+.
|
||||
set_user_data = true,
|
||||
|
||||
--Set the current open status of the browser in the `file_browser-open` field of the `shared-script-properties` property.
|
||||
--This property is deprecated. When it is removed in mpv v0.37 file-browser will automatically ignore this option.
|
||||
set_shared_script_properties = false,
|
||||
|
||||
---@type 'auto'|'left'|'center'|'right'
|
||||
align_x = 'left',
|
||||
---@type 'auto'|'top'|'center'|'bottom'
|
||||
align_y = 'top',
|
||||
|
||||
--style settings
|
||||
format_string_header = [[{\fnMonospace}[%i/%x]%^ %q\N------------------------------------------------------------------]],
|
||||
format_string_topwrapper = '...',
|
||||
format_string_bottomwrapper = '...',
|
||||
|
||||
font_bold_header = true,
|
||||
font_opacity_selection_marker = "99",
|
||||
|
||||
scaling_factor_base = 1,
|
||||
scaling_factor_header = 1.4,
|
||||
scaling_factor_wrappers = 1,
|
||||
|
||||
font_name_header = "",
|
||||
font_name_body = "",
|
||||
font_name_wrappers = "",
|
||||
font_name_folder = "",
|
||||
font_name_cursor = "",
|
||||
|
||||
font_colour_header = "00ccff",
|
||||
font_colour_body = "ffffff",
|
||||
font_colour_wrappers = "00ccff",
|
||||
font_colour_cursor = "00ccff",
|
||||
font_colour_escape_chars = "413eff",
|
||||
|
||||
font_colour_multiselect = "fcad88",
|
||||
font_colour_selected = "fce788",
|
||||
font_colour_playing = "33ff66",
|
||||
font_colour_playing_multiselected = "22b547"
|
||||
|
||||
}
|
||||
|
||||
opt.read_options(o, 'file_browser')
|
||||
|
||||
---@diagnostic disable-next-line deprecated
|
||||
o.set_shared_script_properties = o.set_shared_script_properties and utils.shared_script_property_set
|
||||
|
||||
return o
|
||||
@@ -1,362 +0,0 @@
|
||||
------------------------------------------------------------------------------------------
|
||||
---------------------------------File/Playlist Opening------------------------------------
|
||||
------------------------------------------------------------------------------------------
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local ass = require 'modules.ass'
|
||||
local cursor = require 'modules.navigation.cursor'
|
||||
local controls = require 'modules.controls'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
local movement = require 'modules.navigation.directory-movement'
|
||||
|
||||
local state = g.state
|
||||
|
||||
---@alias LoadfileFlag 'replace'|'append-play'
|
||||
|
||||
---@class LoadOpts
|
||||
---@field directory string
|
||||
---@field flag LoadfileFlag
|
||||
---@field autoload boolean
|
||||
---@field items_appended number
|
||||
---@field co thread
|
||||
---@field concurrency number
|
||||
|
||||
---In mpv v0.38 a new index argument was added to the loadfile command.
|
||||
---For some crazy reason this new argument is placed before the existing options
|
||||
---argument, breaking any scripts that used it. This function finds the correct index
|
||||
---for the options argument using the `command-list` property.
|
||||
---@return integer
|
||||
local function get_loadfile_options_arg_index()
|
||||
---@type table[]
|
||||
local command_list = mp.get_property_native('command-list', {})
|
||||
for _, command in ipairs(command_list) do
|
||||
if command.name == 'loadfile' then
|
||||
for i, arg in ipairs(command.args or {} --[=[@as table[]]=]) do
|
||||
if arg.name == 'options' then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 3
|
||||
end
|
||||
|
||||
local LEGACY_LOADFILE_SYNTAX = get_loadfile_options_arg_index() == 3
|
||||
|
||||
---A wrapper around loadfile to handle the syntax changes introduced in mpv v0.38.
|
||||
---@param file string
|
||||
---@param flag string
|
||||
---@param options? string|table<string,unknown>
|
||||
---@return boolean
|
||||
local function legacy_loadfile_wrapper(file, flag, options)
|
||||
if LEGACY_LOADFILE_SYNTAX then
|
||||
return mp.command_native({"loadfile", file, flag, options}) ~= nil
|
||||
else
|
||||
return mp.command_native({"loadfile", file, flag, -1, options}) ~= nil
|
||||
end
|
||||
end
|
||||
|
||||
---Adds a file to the playlist and changes the flag to `append-play` in preparation for future items.
|
||||
---@param file string
|
||||
---@param opts LoadOpts
|
||||
---@param mpv_opts? string|table<string,unknown>
|
||||
local function loadfile(file, opts, mpv_opts)
|
||||
if o.substitute_backslash and not fb_utils.get_protocol(file) then
|
||||
file = string.gsub(file, "/", "\\")
|
||||
end
|
||||
|
||||
if opts.flag == "replace" then msg.verbose("Playling file", file)
|
||||
else msg.verbose("Appending", file, "to the playlist") end
|
||||
|
||||
if mpv_opts then
|
||||
msg.debug('Settings opts on', file, ':', utils.to_string(mpv_opts))
|
||||
end
|
||||
|
||||
if not legacy_loadfile_wrapper(file, opts.flag, mpv_opts) then msg.warn(file) end
|
||||
if opts.flag == 'replace' and mp.get_property_bool('pause') then mp.set_property_bool('pause', false) end
|
||||
|
||||
opts.flag = "append-play"
|
||||
opts.items_appended = opts.items_appended + 1
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line no-unknown
|
||||
local concurrent_loadlist_wrapper
|
||||
|
||||
---@alias ConcurrentRefMap table<List|Item,{directory: string?, sublist: List?, recurse: boolean?}>
|
||||
|
||||
---This function recursively loads directories concurrently in separate coroutines.
|
||||
---Results are saved in a tree of tables that allows asynchronous access.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param load_opts LoadOpts
|
||||
---@param prev_dirs Set<string>
|
||||
---@param item_t Item
|
||||
---@param refs ConcurrentRefMap
|
||||
---@return boolean?
|
||||
local function concurrent_loadlist_parse(directory, load_opts, prev_dirs, item_t, refs)
|
||||
if not refs[item_t] then refs[item_t] = {} end
|
||||
|
||||
--prevents infinite recursion from the item.path or opts.directory fields
|
||||
if prev_dirs[directory] then return end
|
||||
prev_dirs[directory] = true
|
||||
|
||||
local list, list_opts = scanning.scan_directory(directory, { source = 'loadlist' })
|
||||
if list == g.root then return end
|
||||
|
||||
--if we can't parse the directory then append it and hope mpv fares better
|
||||
if list == nil then
|
||||
msg.warn("Could not parse", directory, "appending to playlist anyway")
|
||||
refs[item_t].recurse = false
|
||||
return
|
||||
end
|
||||
|
||||
directory = list_opts.directory or directory
|
||||
|
||||
--we must declare these before we start loading sublists otherwise the append thread will
|
||||
--need to wait until the whole list is loaded (when synchronous IO is used)
|
||||
refs[item_t].sublist = list or {}
|
||||
refs[list] = {directory = directory}
|
||||
|
||||
if directory == "" then return end
|
||||
|
||||
--launches new parse operations for directories, each in a different coroutine
|
||||
for _, item in ipairs(list) do
|
||||
if fb_utils.parseable_item(item) then
|
||||
fb_utils.coroutine.run(concurrent_loadlist_wrapper, fb_utils.get_new_directory(item, directory), load_opts, prev_dirs, item, refs)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---A wrapper function that ensures the concurrent_loadlist_parse is run correctly.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param opts LoadOpts
|
||||
---@param prev_dirs Set<string>
|
||||
---@param item Item
|
||||
---@param refs ConcurrentRefMap
|
||||
function concurrent_loadlist_wrapper(directory, opts, prev_dirs, item, refs)
|
||||
--ensures that only a set number of concurrent parses are operating at any one time.
|
||||
--the mpv event queue is seemingly limited to 1000 items, but only async mpv actions like
|
||||
--command_native_async should use that, events like mp.add_timeout (which coroutine.sleep() uses) should
|
||||
--be handled enturely on the Lua side with a table, which has a significantly larger maximum size.
|
||||
while (opts.concurrency > o.max_concurrency) do
|
||||
fb_utils.coroutine.sleep(0.1)
|
||||
end
|
||||
opts.concurrency = opts.concurrency + 1
|
||||
|
||||
local success = concurrent_loadlist_parse(directory, opts, prev_dirs, item, refs)
|
||||
opts.concurrency = opts.concurrency - 1
|
||||
if not success then refs[item].sublist = {} end
|
||||
if coroutine.status(opts.co) == "suspended" then fb_utils.coroutine.resume_err(opts.co) end
|
||||
end
|
||||
|
||||
---Recursively appends items to the playlist, acts as a consumer to the previous functions producer;
|
||||
---If the next directory has not been parsed this function will yield until the parse has completed.
|
||||
---@async
|
||||
---@param list List
|
||||
---@param load_opts LoadOpts
|
||||
---@param refs ConcurrentRefMap
|
||||
local function concurrent_loadlist_append(list, load_opts, refs)
|
||||
local directory = refs[list].directory
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not g.sub_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
and not g.audio_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
then
|
||||
while fb_utils.parseable_item(item) and (not refs[item] or not refs[item].sublist) do
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
if fb_utils.parseable_item(item) and refs[item] ~= false then
|
||||
concurrent_loadlist_append(refs[item].sublist, load_opts, refs)
|
||||
else
|
||||
loadfile(fb_utils.get_full_path(item, directory), load_opts, item.mpv_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Recursive function to load directories serially.
|
||||
---Returns true if any items were appended to the playlist.
|
||||
---@async
|
||||
---@param directory string
|
||||
---@param load_opts LoadOpts
|
||||
---@param prev_dirs Set<string>
|
||||
---@return true|nil
|
||||
local function custom_loadlist_recursive(directory, load_opts, prev_dirs)
|
||||
--prevents infinite recursion from the item.path or opts.directory fields
|
||||
if prev_dirs[directory] then return end
|
||||
prev_dirs[directory] = true
|
||||
|
||||
local list, opts = scanning.scan_directory(directory, { source = "loadlist" })
|
||||
if list == g.root then return end
|
||||
|
||||
--if we can't parse the directory then append it and hope mpv fares better
|
||||
if list == nil then
|
||||
msg.warn("Could not parse", directory, "appending to playlist anyway")
|
||||
loadfile(directory, load_opts)
|
||||
return true
|
||||
end
|
||||
|
||||
directory = opts.directory or directory
|
||||
if directory == "" then return end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not g.sub_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
and not g.audio_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
then
|
||||
if fb_utils.parseable_item(item) then
|
||||
custom_loadlist_recursive( fb_utils.get_new_directory(item, directory) , load_opts, prev_dirs)
|
||||
else
|
||||
local path = fb_utils.get_full_path(item, directory)
|
||||
loadfile(path, load_opts, item.mpv_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---A wrapper for the custom_loadlist_recursive function.
|
||||
---@async
|
||||
---@param item Item
|
||||
---@param opts LoadOpts
|
||||
local function loadlist(item, opts)
|
||||
local dir = fb_utils.get_full_path(item, opts.directory)
|
||||
local num_items = opts.items_appended
|
||||
|
||||
if o.concurrent_recursion then
|
||||
item = fb_utils.copy_table(item)
|
||||
opts.co = fb_utils.coroutine.assert()
|
||||
opts.concurrency = 0
|
||||
|
||||
---@type List
|
||||
local v_list = {item}
|
||||
---@type ConcurrentRefMap
|
||||
local refs = setmetatable({[v_list] = {directory = opts.directory}}, {__mode = 'k'})
|
||||
|
||||
--we need the current coroutine to suspend before we run the first parse operation, so
|
||||
--we schedule the coroutine to run on the mpv event queue
|
||||
fb_utils.coroutine.queue(concurrent_loadlist_wrapper, dir, opts, {}, item, refs)
|
||||
concurrent_loadlist_append(v_list, opts, refs)
|
||||
else
|
||||
custom_loadlist_recursive(dir, opts, {})
|
||||
end
|
||||
|
||||
if opts.items_appended == num_items then msg.warn(dir, "contained no valid files") end
|
||||
end
|
||||
|
||||
---Load playlist entries before and after the currently playing file.
|
||||
---@param path string
|
||||
---@param opts LoadOpts
|
||||
local function autoload_dir(path, opts)
|
||||
if o.autoload_save_current and path == g.current_file.path then
|
||||
mp.commandv("write-watch-later-config") end
|
||||
|
||||
--loads the currently selected file, clearing the playlist in the process
|
||||
loadfile(path, opts)
|
||||
|
||||
local pos = 1
|
||||
local file_count = 0
|
||||
for _,item in ipairs(state.list) do
|
||||
if item.type == "file"
|
||||
and not g.sub_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
and not g.audio_extensions[ fb_utils.get_extension(item.name, "") ]
|
||||
then
|
||||
local p = fb_utils.get_full_path(item)
|
||||
|
||||
if p == path then pos = file_count
|
||||
else loadfile( p, opts, item.mpv_options) end
|
||||
|
||||
file_count = file_count + 1
|
||||
end
|
||||
end
|
||||
mp.commandv("playlist-move", 0, pos+1)
|
||||
end
|
||||
|
||||
---Runs the loadfile or loadlist command.
|
||||
---@async
|
||||
---@param item Item
|
||||
---@param opts LoadOpts
|
||||
---@return nil
|
||||
local function open_item(item, opts)
|
||||
if fb_utils.parseable_item(item) then
|
||||
return loadlist(item, opts)
|
||||
end
|
||||
|
||||
local path = fb_utils.get_full_path(item, opts.directory)
|
||||
if g.sub_extensions[ fb_utils.get_extension(item.name, "") ] then
|
||||
mp.commandv("sub-add", path, opts.flag == "replace" and "select" or "auto")
|
||||
elseif g.audio_extensions[ fb_utils.get_extension(item.name, "") ] then
|
||||
mp.commandv("audio-add", path, opts.flag == "replace" and "select" or "auto")
|
||||
else
|
||||
if opts.autoload then autoload_dir(path, opts)
|
||||
else loadfile(path, opts, item.mpv_options) end
|
||||
end
|
||||
end
|
||||
|
||||
---Handles the open options as a coroutine.
|
||||
---Once loadfile has been run we can no-longer guarantee synchronous execution - the state values may change
|
||||
---therefore, we must ensure that any state values that could be used after a loadfile call are saved beforehand.
|
||||
---@async
|
||||
---@param opts LoadOpts
|
||||
---@return nil
|
||||
local function open_file_coroutine(opts)
|
||||
if not state.list[state.selected] then return end
|
||||
if opts.flag == 'replace' then controls.close() end
|
||||
|
||||
--we want to set the idle option to yes to ensure that if the first item
|
||||
--fails to load then the player has a chance to attempt to load further items (for async append operations)
|
||||
local idle = mp.get_property("idle", "once")
|
||||
mp.set_property("idle", "yes")
|
||||
|
||||
--handles multi-selection behaviour
|
||||
if next(state.selection) then
|
||||
local selection = fb_utils.sort_keys(state.selection)
|
||||
--reset the selection after
|
||||
state.selection = {}
|
||||
|
||||
cursor.disable_select_mode()
|
||||
ass.update_ass()
|
||||
|
||||
--the currently selected file will be loaded according to the flag
|
||||
--the flag variable will be switched to append once a file is loaded
|
||||
for i=1, #selection do
|
||||
open_item(selection[i], opts)
|
||||
end
|
||||
|
||||
else
|
||||
local item = state.list[state.selected]
|
||||
if opts.flag == "replace" then movement.down_dir() end
|
||||
open_item(item, opts)
|
||||
end
|
||||
|
||||
if mp.get_property("idle") == "yes" then mp.set_property("idle", idle) end
|
||||
end
|
||||
|
||||
--opens the selelected file(s)
|
||||
local function open_file(flag, autoload)
|
||||
---@type LoadOpts
|
||||
local opts = {
|
||||
flag = flag,
|
||||
autoload = (autoload ~= o.autoload and flag == "replace"),
|
||||
directory = state.directory,
|
||||
items_appended = 0,
|
||||
concurrency = 0,
|
||||
co = coroutine.create(open_file_coroutine)
|
||||
}
|
||||
fb_utils.coroutine.resume_err(opts.co, opts)
|
||||
end
|
||||
|
||||
---@class playlist
|
||||
return {
|
||||
add_files = open_file,
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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 fb_utils = require 'modules.utils'
|
||||
local scanning = require 'modules.navigation.scanning'
|
||||
|
||||
---@class script_messages
|
||||
local script_messages = {}
|
||||
|
||||
---Allows other scripts to request directory contents from file-browser.
|
||||
---@param directory string
|
||||
---@param response_str string
|
||||
function script_messages.get_directory_contents(directory, response_str)
|
||||
---@async
|
||||
fb_utils.coroutine.run(function()
|
||||
if not directory then msg.error("did not receive a directory string"); return end
|
||||
if not response_str then msg.error("did not receive a response string"); return end
|
||||
|
||||
directory = mp.command_native({"expand-path", directory}, "") --[[@as string]]
|
||||
if directory ~= "" then directory = fb_utils.fix_path(directory, true) end
|
||||
msg.verbose(("recieved %q from 'get-directory-contents' script message - returning result to %q"):format(directory, response_str))
|
||||
|
||||
directory = fb_utils.resolve_directory_mapping(directory)
|
||||
|
||||
---@class OptsWithVersion: Opts
|
||||
---@field API_VERSION string?
|
||||
|
||||
---@type List|nil, OptsWithVersion|Opts|nil
|
||||
local list, opts = scanning.scan_directory(directory, { source = "script-message" } )
|
||||
if opts then opts.API_VERSION = g.API_VERSION end
|
||||
|
||||
local list_str, err = fb_utils.format_json_safe(list)
|
||||
if not list_str then msg.error(err) end
|
||||
|
||||
local opts_str, err2 = fb_utils.format_json_safe(opts)
|
||||
if not opts_str then msg.error(err2) end
|
||||
|
||||
mp.commandv("script-message", response_str, list_str or "", opts_str or "")
|
||||
end)
|
||||
end
|
||||
|
||||
---A helper script message for custom keybinds.
|
||||
---Substitutes any '=>' arguments for 'script-message'.
|
||||
---Makes chaining script-messages much easier.
|
||||
---@param ... string
|
||||
function script_messages.chain(...)
|
||||
---@type string[]
|
||||
local command = table.pack('script-message', ...)
|
||||
for i, v in ipairs(command) do
|
||||
if v == '=>' then command[i] = 'script-message' end
|
||||
end
|
||||
mp.commandv(table.unpack(command))
|
||||
end
|
||||
|
||||
---A helper script message for custom keybinds.
|
||||
---Sends a command after the specified delay.
|
||||
---@param delay string
|
||||
---@param ... string
|
||||
---@return nil
|
||||
function script_messages.delay_command(delay, ...)
|
||||
local command = table.pack(...)
|
||||
local success, err = pcall(mp.add_timeout, fb_utils.evaluate_string('return '..delay), function() mp.commandv(table.unpack(command)) end)
|
||||
if not success then return msg.error(err) end
|
||||
end
|
||||
|
||||
---A helper script message for custom keybinds.
|
||||
---Sends a command only if the given expression returns true.
|
||||
---@param condition string
|
||||
---@param ... string
|
||||
function script_messages.conditional_command(condition, ...)
|
||||
local command = table.pack(...)
|
||||
fb_utils.coroutine.run(function()
|
||||
if fb_utils.evaluate_string('return '..condition) == true then mp.commandv(table.unpack(command)) end
|
||||
end)
|
||||
end
|
||||
|
||||
---A helper script message for custom keybinds.
|
||||
---Extracts lua expressions from the command and evaluates them.
|
||||
---Expressions must be surrounded by !{}. Another ! before the { will escape the evaluation.
|
||||
---@param ... string
|
||||
function script_messages.evaluate_expressions(...)
|
||||
---@type string[]
|
||||
local args = table.pack(...)
|
||||
fb_utils.coroutine.run(function()
|
||||
for i, arg in ipairs(args) do
|
||||
args[i] = arg:gsub('(!+)(%b{})', function(lead, expression)
|
||||
if #lead % 2 == 0 then return string.rep('!', #lead/2)..expression end
|
||||
|
||||
---@type any
|
||||
local eval = fb_utils.evaluate_string('return '..expression:sub(2, -2))
|
||||
return type(eval) == "table" and utils.to_string(eval) or tostring(eval)
|
||||
end)
|
||||
end
|
||||
|
||||
mp.commandv(table.unpack(args))
|
||||
end)
|
||||
end
|
||||
|
||||
---A helper function for custom-keybinds.
|
||||
---Concatenates the command arguments with newlines and runs the
|
||||
---string as a statement of code.
|
||||
---@param ... string
|
||||
function script_messages.run_statement(...)
|
||||
local statement = table.concat(table.pack(...), '\n')
|
||||
fb_utils.coroutine.run(fb_utils.evaluate_string, statement)
|
||||
end
|
||||
|
||||
return script_messages
|
||||
@@ -1,60 +0,0 @@
|
||||
local mp = require 'mp'
|
||||
|
||||
local o = require 'modules.options'
|
||||
local g = require 'modules.globals'
|
||||
local fb_utils = require 'modules.utils'
|
||||
local fb = require 'modules.apis.fb'
|
||||
|
||||
--sets up the compatible extensions list
|
||||
local function setup_extensions_list()
|
||||
--setting up subtitle extensions
|
||||
for ext in fb_utils.iterate_opt(o.subtitle_extensions:lower(), ',') do
|
||||
g.sub_extensions[ext] = true
|
||||
g.extensions[ext] = true
|
||||
end
|
||||
|
||||
--setting up audio extensions
|
||||
for ext in fb_utils.iterate_opt(o.audio_extensions:lower(), ',') do
|
||||
g.audio_extensions[ext] = true
|
||||
g.extensions[ext] = true
|
||||
end
|
||||
|
||||
--adding file extensions to the set
|
||||
for _, ext in ipairs(g.compatible_file_extensions) do
|
||||
g.extensions[ext] = true
|
||||
end
|
||||
|
||||
--adding extra extensions on the whitelist
|
||||
for str in fb_utils.iterate_opt(o.extension_whitelist:lower(), ',') do
|
||||
g.extensions[str] = true
|
||||
end
|
||||
|
||||
--removing extensions that are in the blacklist
|
||||
for str in fb_utils.iterate_opt(o.extension_blacklist:lower(), ',') do
|
||||
g.extensions[str] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--splits the string into a table on the separators
|
||||
local function setup_root()
|
||||
for str in fb_utils.iterate_opt(o.root) do
|
||||
local path = mp.command_native({'expand-path', str}) --[[@as string]]
|
||||
path = fb_utils.fix_path(path, true)
|
||||
|
||||
local temp = {name = path, type = 'dir', label = str, ass = fb_utils.ass_escape(str, true)}
|
||||
|
||||
g.root[#g.root+1] = temp
|
||||
end
|
||||
|
||||
if g.PLATFORM == 'windows' then
|
||||
fb.register_root_item('C:/')
|
||||
elseif g.PLATFORM ~= nil then
|
||||
fb.register_root_item('/')
|
||||
end
|
||||
end
|
||||
|
||||
---@class setup
|
||||
return {
|
||||
extensions_list = setup_extensions_list,
|
||||
root = setup_root,
|
||||
}
|
||||
@@ -1,637 +0,0 @@
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
-----------------------------------------Utility Functions----------------------------------------------
|
||||
---------------------------------------Part of the addon API--------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
|
||||
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 input_loaded, input = pcall(require, 'mp.input')
|
||||
local user_input_loaded, user_input = pcall(require, 'user-input-module')
|
||||
|
||||
--creates a table for the API functions
|
||||
--adds one metatable redirect to prevent addon authors from accidentally breaking file-browser
|
||||
---@class fb_utils
|
||||
local fb_utils = { API_VERSION = g.API_VERSION }
|
||||
|
||||
fb_utils.list = {}
|
||||
fb_utils.coroutine = {}
|
||||
|
||||
--implements table.pack if on lua 5.1
|
||||
if not table.pack then
|
||||
table.unpack = unpack ---@diagnostic disable-line deprecated
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function table.pack(...)
|
||||
local t = {n = select("#", ...), ...}
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
---Returns the index of the given item in the table.
|
||||
---Return -1 if item does not exist.
|
||||
---@generic T
|
||||
---@param t T[]
|
||||
---@param item T
|
||||
---@param from_index? number
|
||||
---@return integer
|
||||
function fb_utils.list.indexOf(t, item, from_index)
|
||||
for i = from_index or 1, #t, 1 do
|
||||
if t[i] == item then return i end
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
---Returns whether or not the given table contains an entry that
|
||||
---causes the given function to evaluate to true.
|
||||
---@generic T
|
||||
---@param t T[]
|
||||
---@param fn fun(v: T, i: number, t: T[]): boolean
|
||||
---@return boolean
|
||||
function fb_utils.list.some(t, fn)
|
||||
for i, v in ipairs(t --[=[@as any[]]=]) do
|
||||
if fn(v, i, t) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Creates a new table populated with the results of
|
||||
---calling a provided function on every element in t.
|
||||
---@generic T
|
||||
---@generic R
|
||||
---@param t T[]
|
||||
---@param fn fun(v: T, i: number, t: T[]): R
|
||||
---@return R[]
|
||||
function fb_utils.list.map(t, fn)
|
||||
local new_t = {}
|
||||
for i, v in ipairs(t --[=[@as any[]]=]) do
|
||||
new_t[i] = fn(v, i, t) ---@diagnostic disable-line no-unknown
|
||||
end
|
||||
return new_t
|
||||
end
|
||||
|
||||
---Prints an error message and a stack trace.
|
||||
---Can be passed directly to xpcall.
|
||||
---@param errmsg string
|
||||
---@param co? thread A coroutine to grab the stack trace from.
|
||||
function fb_utils.traceback(errmsg, co)
|
||||
if co then
|
||||
msg.warn(debug.traceback(co))
|
||||
else
|
||||
msg.warn(debug.traceback("", 2))
|
||||
end
|
||||
msg.error(errmsg)
|
||||
end
|
||||
|
||||
---Returns a table that stores the given table t as the __index in its metatable.
|
||||
---Creates a prototypally inherited table.
|
||||
---@generic T: table
|
||||
---@param t T
|
||||
---@return T
|
||||
function fb_utils.redirect_table(t)
|
||||
return setmetatable({}, { __index = t })
|
||||
end
|
||||
|
||||
---Sets the given table `proto` as the `__index` field in table `t`s metatable.
|
||||
---@generic T: table
|
||||
---@param t T
|
||||
---@param proto table
|
||||
---@return T
|
||||
function fb_utils.set_prototype(t, proto)
|
||||
return setmetatable(t, { __index = proto })
|
||||
end
|
||||
|
||||
---Prints an error if a coroutine returns an error.
|
||||
---Unlike coroutine.resume_err this still returns the results of coroutine.resume().
|
||||
---@param ... any
|
||||
---@return boolean
|
||||
---@return ...
|
||||
function fb_utils.coroutine.resume_catch(...)
|
||||
local returns = table.pack(coroutine.resume(...))
|
||||
if not returns[1] and returns[2] ~= g.ABORT_ERROR then
|
||||
fb_utils.traceback(returns[2], select(1, ...))
|
||||
end
|
||||
return table.unpack(returns, 1, returns.n)
|
||||
end
|
||||
|
||||
---Resumes a coroutine and prints an error if it was not sucessful.
|
||||
---@param ... any
|
||||
---@return boolean
|
||||
function fb_utils.coroutine.resume_err(...)
|
||||
local success, err = coroutine.resume(...)
|
||||
if not success and err ~= g.ABORT_ERROR then
|
||||
fb_utils.traceback(err, select(1, ...))
|
||||
end
|
||||
return success
|
||||
end
|
||||
|
||||
---Throws an error if not run from within a coroutine.
|
||||
---In lua 5.1 there is only one return value which will be nil if run from the main thread.
|
||||
---In lua 5.2 main will be true if running from the main thread.
|
||||
---@param err any
|
||||
---@return thread
|
||||
function fb_utils.coroutine.assert(err)
|
||||
local co, main = coroutine.running()
|
||||
assert(not main and co, err or "error - function must be executed from within a coroutine")
|
||||
return co
|
||||
end
|
||||
|
||||
---Creates a callback function to resume the current coroutine with the given time limit.
|
||||
---If the time limit expires the coroutine will be resumed. The first return value will be true
|
||||
---if the callback was resumed within the time limit and false otherwise.
|
||||
---If time_limit is falsy then there will be no time limit and there will be no additional return value.
|
||||
---@param time_limit? number seconds
|
||||
---@return fun(...)
|
||||
function fb_utils.coroutine.callback(time_limit)
|
||||
local co = fb_utils.coroutine.assert("cannot create a coroutine callback for the main thread")
|
||||
local timer = time_limit and mp.add_timeout(time_limit, function ()
|
||||
msg.debug("time limit on callback expired")
|
||||
fb_utils.coroutine.resume_err(co, false)
|
||||
end)
|
||||
|
||||
local function fn(...)
|
||||
if timer then
|
||||
if not timer:is_enabled() then return
|
||||
else timer:kill() end
|
||||
return fb_utils.coroutine.resume_err(co, true, ...)
|
||||
end
|
||||
return fb_utils.coroutine.resume_err(co, ...)
|
||||
end
|
||||
return fn
|
||||
end
|
||||
|
||||
---Puts the current coroutine to sleep for the given number of seconds.
|
||||
---@async
|
||||
---@param n number
|
||||
---@return nil
|
||||
function fb_utils.coroutine.sleep(n)
|
||||
mp.add_timeout(n, fb_utils.coroutine.callback())
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
---Runs the given function in a coroutine, passing through any additional arguments.
|
||||
---Does not run the coroutine immediately, instead it queues the coroutine to run when the thread is next idle.
|
||||
---Returns the coroutine object so that the caller can act on it before it is run.
|
||||
---@param fn async fun()
|
||||
---@param ... any
|
||||
---@return thread
|
||||
function fb_utils.coroutine.queue(fn, ...)
|
||||
local co = coroutine.create(fn)
|
||||
local args = table.pack(...)
|
||||
mp.add_timeout(0, function() fb_utils.coroutine.resume_err(co, table.unpack(args, 1, args.n)) end)
|
||||
return co
|
||||
end
|
||||
|
||||
---Runs the given function in a coroutine, passing through any additional arguments.
|
||||
---This is for triggering an event in a coroutine.
|
||||
---@param fn async fun()
|
||||
---@param ... any
|
||||
function fb_utils.coroutine.run(fn, ...)
|
||||
local co = coroutine.create(fn)
|
||||
fb_utils.coroutine.resume_err(co, ...)
|
||||
end
|
||||
|
||||
---Get the full path for the current file.
|
||||
---@param item Item
|
||||
---@param dir? string
|
||||
---@return string
|
||||
function fb_utils.get_full_path(item, dir)
|
||||
if item.path then return item.path end
|
||||
return (dir or g.state.directory)..item.name
|
||||
end
|
||||
|
||||
---Gets the path for a new subdirectory, redirects if the path field is set.
|
||||
---Returns the new directory path and a boolean specifying if a redirect happened.
|
||||
---@param item Item
|
||||
---@param directory string
|
||||
---@return string new_directory
|
||||
---@return boolean? redirected `true` if the path was redirected
|
||||
function fb_utils.get_new_directory(item, directory)
|
||||
if item.path and item.redirect ~= false then return item.path, true end
|
||||
if directory == "" then return item.name end
|
||||
if string.sub(directory, -1) == "/" then return directory..item.name end
|
||||
return directory.."/"..item.name
|
||||
end
|
||||
|
||||
---Returns the file extension of the given file, or def if there is none.
|
||||
---@generic T
|
||||
---@param filename string
|
||||
---@param def? T
|
||||
---@return string|T
|
||||
---@overload fun(filename: string): string|nil
|
||||
function fb_utils.get_extension(filename, def)
|
||||
return string.lower(filename):match("%.([^%./]+)$") or def
|
||||
end
|
||||
|
||||
---Returns the protocol scheme of the given url, or def if there is none.
|
||||
---@generic T
|
||||
---@param filename string
|
||||
---@param def T
|
||||
---@return string|T
|
||||
---@overload fun(filename: string): string|nil
|
||||
function fb_utils.get_protocol(filename, def)
|
||||
return string.lower(filename):match("^(%a[%w+-.]*)://") or def
|
||||
end
|
||||
|
||||
---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.
|
||||
---@param str string
|
||||
---@param replace_newline? true|string
|
||||
---@return string
|
||||
function fb_utils.ass_escape(str, replace_newline)
|
||||
if replace_newline == true then replace_newline = "\\\239\187\191n" end
|
||||
|
||||
--escape the invalid single characters
|
||||
str = string.gsub(str, '[\\{}\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 = string.gsub(str, "\\N", replace_newline)
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
---Escape lua pattern characters.
|
||||
---@param str string
|
||||
---@return string
|
||||
function fb_utils.pattern_escape(str)
|
||||
return (string.gsub(str, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
|
||||
end
|
||||
|
||||
---Standardises filepaths across systems.
|
||||
---@param str string
|
||||
---@param is_directory? boolean
|
||||
---@return string
|
||||
function fb_utils.fix_path(str, is_directory)
|
||||
if str == '' then return str end
|
||||
if o.normalise_backslash == 'yes' or (o.normalise_backslash == 'auto' and g.PLATFORM == 'windows') then
|
||||
str = string.gsub(str, [[\]],[[/]])
|
||||
end
|
||||
str = str:gsub([[/%./]], [[/]])
|
||||
if is_directory and str:sub(-1) ~= '/' then str = str..'/' end
|
||||
return str
|
||||
end
|
||||
|
||||
---Wrapper for mp.utils.join_path to handle protocols.
|
||||
---@param working string
|
||||
---@param relative string
|
||||
---@return string
|
||||
function fb_utils.join_path(working, relative)
|
||||
return fb_utils.get_protocol(relative) and relative or utils.join_path(working, relative)
|
||||
end
|
||||
|
||||
---Converts the given path into an absolute path and normalises it using fb_utils.fix_path.
|
||||
---@param path string
|
||||
---@return string
|
||||
function fb_utils.absolute_path(path)
|
||||
local absolute_path = fb_utils.join_path(mp.get_property('working-directory', ''), path)
|
||||
return fb_utils.fix_path(absolute_path)
|
||||
end
|
||||
|
||||
---Sorts the table lexicographically ignoring case and accounting for leading/non-leading zeroes.
|
||||
---The number format functionality was proposed by github user twophyro, and was presumably taken
|
||||
---from here: http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua.
|
||||
---@param t List
|
||||
---@return List
|
||||
function fb_utils.sort(t)
|
||||
local function padnum(n, d)
|
||||
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
|
||||
or ("%03d%s"):format(#n, n)
|
||||
end
|
||||
|
||||
--appends the letter d or f to the start of the comparison to sort directories and folders as well
|
||||
---@type [string,Item][]
|
||||
local tuples = {}
|
||||
for i, f in ipairs(t) do
|
||||
tuples[i] = {f.type:sub(1, 1) .. (f.label or f.name):lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
|
||||
end
|
||||
table.sort(tuples, function(a, b)
|
||||
-- pretty sure that `#b[2] < #a[2]` does not do anything as they are both Item tables and not strings or arrays
|
||||
return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
|
||||
end)
|
||||
for i, tuple in ipairs(tuples) do t[i] = tuple[2] end
|
||||
return t
|
||||
end
|
||||
|
||||
---@param dir string
|
||||
---@return boolean
|
||||
function fb_utils.valid_dir(dir)
|
||||
if o.filter_dot_dirs == 'yes' or o.filter_dot_dirs == 'auto' and g.PLATFORM ~= 'windows' then
|
||||
return string.sub(dir, 1, 1) ~= "."
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param file string
|
||||
---@return boolean
|
||||
function fb_utils.valid_file(file)
|
||||
if o.filter_dot_files == 'yes' or o.filter_dot_files == 'auto' and g.PLATFORM ~= 'windows' then
|
||||
if string.sub(file, 1, 1) == "." then return false end
|
||||
end
|
||||
if o.filter_files and not g.extensions[ fb_utils.get_extension(file, "") ] then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
---Returns whether or not the item can be parsed.
|
||||
---@param item Item
|
||||
---@return boolean
|
||||
function fb_utils.parseable_item(item)
|
||||
return item.type == "dir" or g.parseable_extensions[fb_utils.get_extension(item.name, "")]
|
||||
end
|
||||
|
||||
---Takes a directory string and resolves any directory mappings,
|
||||
---returning the resolved directory.
|
||||
---@param path string
|
||||
---@return string
|
||||
function fb_utils.resolve_directory_mapping(path)
|
||||
if not path then return path end
|
||||
|
||||
for mapping, target in pairs(g.directory_mappings) do
|
||||
local start, finish = string.find(path, mapping)
|
||||
if start then
|
||||
msg.debug('mapping', mapping, 'found for', path, 'changing to', target)
|
||||
|
||||
-- if the mapping is an exact match then return the target as is
|
||||
if finish == #path then return target end
|
||||
|
||||
-- else make sure the path is correctly formatted
|
||||
target = fb_utils.fix_path(target, true)
|
||||
return (string.gsub(path, mapping, target))
|
||||
end
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
---Removes items and folders from the list that fail the configured filters.
|
||||
---@param t List
|
||||
---@return List
|
||||
function fb_utils.filter(t)
|
||||
local max = #t
|
||||
local top = 1
|
||||
for i = 1, max do
|
||||
local temp = t[i]
|
||||
t[i] = nil
|
||||
|
||||
if ( temp.type == "dir" and fb_utils.valid_dir(temp.label or temp.name) ) or
|
||||
( temp.type == "file" and fb_utils.valid_file(temp.label or temp.name) )
|
||||
then
|
||||
t[top] = temp
|
||||
top = top+1
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
---Returns a string iterator that uses the root separators.
|
||||
---@param str any
|
||||
---@param separators? string Override the root separators.
|
||||
---@return fun():(string, ...)
|
||||
function fb_utils.iterate_opt(str, separators)
|
||||
return string.gmatch(str, "([^"..fb_utils.pattern_escape(separators or o.root_separators).."]+)")
|
||||
end
|
||||
|
||||
---Sorts a table into an array of selected items in the correct order.
|
||||
---If a predicate function is passed, then the item will only be added to
|
||||
---the table if the function returns true.
|
||||
---@param t Set<number>
|
||||
---@param include_item? fun(item: Item): boolean
|
||||
---@return Item[]
|
||||
function fb_utils.sort_keys(t, include_item)
|
||||
---@class Ref
|
||||
---@field item Item
|
||||
---@field index number
|
||||
|
||||
---@type Ref[]
|
||||
local keys = {}
|
||||
for k in pairs(t) do
|
||||
local item = g.state.list[k]
|
||||
if not include_item or include_item(item) then
|
||||
keys[#keys+1] = {
|
||||
item = item,
|
||||
index = k,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(keys, function(a,b) return a.index < b.index end)
|
||||
return fb_utils.list.map(keys, function(ref) return ref.item end)
|
||||
end
|
||||
|
||||
---Uses a loop to get the length of an array. The `#` operator is undefined if there
|
||||
---are gaps in the array, this ensures there are none as expected by the mpv node function.
|
||||
---@param t any[]
|
||||
---@return integer
|
||||
local function get_length(t)
|
||||
local i = 1
|
||||
while t[i] do i = i+1 end
|
||||
return i - 1
|
||||
end
|
||||
|
||||
---Recursively removes elements of the table which would cause
|
||||
---utils.format_json to throw an error.
|
||||
---@generic T
|
||||
---@param t T
|
||||
---@return T
|
||||
local function json_safe_recursive(t)
|
||||
if type(t) ~= "table" then return t end
|
||||
|
||||
local array_length = get_length(t)
|
||||
local isarray = array_length > 0
|
||||
|
||||
for key, value in pairs(t --[[@as table<any,any>]]) do
|
||||
local ktype = type(key)
|
||||
local vtype = type(value)
|
||||
|
||||
if vtype ~= "userdata" and vtype ~= "function" and vtype ~= "thread"
|
||||
and (( isarray and ktype == "number" and key <= array_length)
|
||||
or (not isarray and ktype == "string"))
|
||||
then
|
||||
---@diagnostic disable-next-line no-unknown
|
||||
t[key] = json_safe_recursive(t[key])
|
||||
elseif key then
|
||||
---@diagnostic disable-next-line no-unknown
|
||||
t[key] = nil
|
||||
if isarray then array_length = get_length(t) end
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
---Formats a table into a json string but ensures there are no invalid datatypes inside the table first.
|
||||
---@param t any
|
||||
---@return string|nil
|
||||
---@return string|nil err
|
||||
function fb_utils.format_json_safe(t)
|
||||
--operate on a copy of the table to prevent any data loss in the original table
|
||||
t = json_safe_recursive(fb_utils.copy_table(t))
|
||||
local success, result, err = pcall(utils.format_json, t)
|
||||
if success then return result, err
|
||||
else return nil, result end
|
||||
end
|
||||
|
||||
---Evaluates and runs the given string in both Lua 5.1 and 5.2.
|
||||
---Provides the mpv modules and the fb module to the string.
|
||||
---@param str string
|
||||
---@param chunkname? string Used for error reporting.
|
||||
---@param custom_env? table A custom environment that shadows the default environment.
|
||||
---@param env_defaults? boolean Load lua defaults in environment, as well as mpv and file-browser modules. Defaults to `true`.
|
||||
---@return unknown
|
||||
function fb_utils.evaluate_string(str, chunkname, custom_env, env_defaults)
|
||||
---@type table
|
||||
local env
|
||||
if env_defaults ~= false then
|
||||
---@type table
|
||||
env = fb_utils.redirect_table(_G)
|
||||
env.mp = fb_utils.redirect_table(mp)
|
||||
env.msg = fb_utils.redirect_table(msg)
|
||||
env.utils = fb_utils.redirect_table(utils)
|
||||
env.fb = fb_utils.redirect_table(require 'file-browser')
|
||||
env.input = input_loaded and fb_utils.redirect_table(input)
|
||||
env.user_input = user_input_loaded and fb_utils.redirect_table(user_input)
|
||||
env = fb_utils.set_prototype(custom_env or {}, env)
|
||||
else
|
||||
env = custom_env or {}
|
||||
end
|
||||
|
||||
---@type function, any
|
||||
local chunk, err
|
||||
if setfenv then ---@diagnostic disable-line deprecated
|
||||
chunk, err = loadstring(str, chunkname) ---@diagnostic disable-line deprecated
|
||||
if chunk then setfenv(chunk, env) end ---@diagnostic disable-line deprecated
|
||||
else
|
||||
chunk, err = load(str, chunkname, 't', env) ---@diagnostic disable-line redundant-parameter
|
||||
end
|
||||
if not chunk then
|
||||
msg.warn('failed to load string:', str)
|
||||
msg.error(err)
|
||||
chunk = function() return nil end
|
||||
end
|
||||
|
||||
return chunk()
|
||||
end
|
||||
|
||||
---Copies a table without leaving any references to the original.
|
||||
---Uses a structured clone algorithm to maintain cyclic references.
|
||||
---@generic T
|
||||
---@param t T
|
||||
---@param references table<table,table>
|
||||
---@param depth number
|
||||
---@return T
|
||||
local function copy_table_recursive(t, references, depth)
|
||||
if type(t) ~= "table" or depth == 0 then return t end
|
||||
if references[t] then return references[t] end
|
||||
|
||||
local copy = setmetatable({}, { __original = t })
|
||||
references[t] = copy
|
||||
|
||||
for key, value in pairs(t --[[@as table<any,any>]]) do
|
||||
key = copy_table_recursive(key, references, depth - 1)
|
||||
copy[key] = copy_table_recursive(value, references, depth - 1) ---@diagnostic disable-line no-unknown
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
---A wrapper around copy_table to provide the reference table.
|
||||
---@generic T
|
||||
---@param t T
|
||||
---@param depth? number
|
||||
---@return T
|
||||
function fb_utils.copy_table(t, depth)
|
||||
--this is to handle cyclic table references
|
||||
return copy_table_recursive(t, {}, depth or math.huge)
|
||||
end
|
||||
|
||||
---@alias Replacer fun(item: Item, s: State): (string|number|nil)
|
||||
---@alias ReplacerTable table<string,Replacer>
|
||||
|
||||
---functions to replace custom-keybind codes
|
||||
---@type ReplacerTable
|
||||
fb_utils.code_fns = {
|
||||
["%"] = function() return "%" end,
|
||||
|
||||
f = function(item, s) return item and fb_utils.get_full_path(item, s.directory) or "" end,
|
||||
n = function(item, s) return item and (item.label or item.name) or "" end,
|
||||
i = function(item, s)
|
||||
local i = fb_utils.list.indexOf(s.list, item)
|
||||
if #s.list == 0 then return 0 end
|
||||
return ('%0'..math.ceil(math.log10(#s.list))..'d'):format(i ~= -1 and i or 0) ---@diagnostic disable-line deprecated
|
||||
end,
|
||||
j = function (item, s)
|
||||
return fb_utils.list.indexOf(s.list, item) ~= -1 and math.abs(fb_utils.list.indexOf( fb_utils.sort_keys(s.selection) , item)) or 0
|
||||
end,
|
||||
x = function(_, s) return #s.list or 0 end,
|
||||
p = function(_, s) return s.directory or "" end,
|
||||
q = function(_, s) return s.directory == '' and 'ROOT' or s.directory_label or s.directory or "" end,
|
||||
d = function(_, s) return (s.directory_label or s.directory):match("([^/]+)/?$") or "" end,
|
||||
r = function(_, s) return s.parser.keybind_name or s.parser.name or "" end,
|
||||
}
|
||||
|
||||
---Programatically creates a pattern that matches any key code.
|
||||
---This will result in some duplicates but that shouldn't really matter.
|
||||
---@param codes ReplacerTable
|
||||
---@return string
|
||||
function fb_utils.get_code_pattern(codes)
|
||||
---@type string
|
||||
local CUSTOM_KEYBIND_CODES = ""
|
||||
for key in pairs(codes) do CUSTOM_KEYBIND_CODES = CUSTOM_KEYBIND_CODES..key:lower()..key:upper() end
|
||||
for key in pairs((getmetatable(codes) or {}).__index or {} --[[@as ReplacerTable]]) do
|
||||
---@type string
|
||||
CUSTOM_KEYBIND_CODES = CUSTOM_KEYBIND_CODES..key:lower()..key:upper()
|
||||
end
|
||||
return('%%%%([%s])'):format(fb_utils.pattern_escape(CUSTOM_KEYBIND_CODES))
|
||||
end
|
||||
|
||||
---Substitutes codes in the given string for other substrings.
|
||||
---@param str string
|
||||
---@param overrides? ReplacerTable Replacer functions for additional characters to match to after `%` characters.
|
||||
---@param item? Item Uses the currently selected item if nil.
|
||||
---@param state? State Uses the global state if nil.
|
||||
---@param modifier_fn? fun(new_str: string, code: string): string given the replacement substrings before they are placed in the main string
|
||||
--- (the return value is the new replacement string).
|
||||
---@return string
|
||||
function fb_utils.substitute_codes(str, overrides, item, state, modifier_fn)
|
||||
local replacers = overrides and setmetatable(fb_utils.copy_table(overrides), {__index = fb_utils.code_fns}) or fb_utils.code_fns
|
||||
item = item or g.state.list[g.state.selected]
|
||||
state = state or g.state
|
||||
|
||||
return (string.gsub(str, fb_utils.get_code_pattern(replacers), function(code)
|
||||
---@type string|number|nil
|
||||
local result
|
||||
local replacer = replacers[code]
|
||||
|
||||
if type(replacer) == "string" then
|
||||
result = replacer
|
||||
--encapsulates the string if using an uppercase code
|
||||
elseif not replacer then
|
||||
local lower_fn = replacers[code:lower()]
|
||||
if not lower_fn then return end
|
||||
result = string.format("%q", lower_fn(item, state))
|
||||
else
|
||||
result = replacer(item, state)
|
||||
end
|
||||
|
||||
if result and modifier_fn then return modifier_fn(tostring(result), code) end
|
||||
return result
|
||||
end))
|
||||
end
|
||||
|
||||
|
||||
return fb_utils
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 975 KiB |
+18
-63
@@ -1,4 +1,3 @@
|
||||
local msg = require "mp.msg"
|
||||
local utils = require "mp.utils"
|
||||
local legacy = mp.command_native_async == nil
|
||||
local config = {}
|
||||
@@ -45,55 +44,22 @@ function apply_defaults(info)
|
||||
return info
|
||||
end
|
||||
|
||||
local function build_directory_string(dir, repo)
|
||||
local str = ""
|
||||
local contents = utils.readdir(dir)
|
||||
if not contents then return msg.error("could not access local repo:", repo) end
|
||||
for _, item in ipairs(contents) do
|
||||
local path = dir..'/'..item
|
||||
if utils.file_info(path).is_dir then
|
||||
if item ~= ".git" then str = str..'/'..build_directory_string(path, repo)..'\n' end
|
||||
else
|
||||
str = str..(path:sub(repo:len()+2))..'\n'
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function get_file_list(info)
|
||||
if not info.local_repo then
|
||||
return run({"git", "-C", info.edist, "ls-tree", "-r", "--name-only", "remotes/manager/"..info.branch}).stdout
|
||||
else
|
||||
return build_directory_string(info.local_repo, info.local_repo)
|
||||
end
|
||||
end
|
||||
|
||||
function update(info)
|
||||
info = apply_defaults(info)
|
||||
if not info then return false end
|
||||
|
||||
local base = nil
|
||||
|
||||
info.edist = string.match(mp.command_native({"expand-path", info.dest}), "(.-)[/\\]?$")
|
||||
mkdir(info.edist)
|
||||
|
||||
|
||||
local e_dest = string.match(mp.command_native({"expand-path", info.dest}), "(.-)[/\\]?$")
|
||||
mkdir(e_dest)
|
||||
|
||||
local files = {}
|
||||
|
||||
if info.local_repo then
|
||||
info.local_repo = mp.command_native({"expand-path", info.local_repo})
|
||||
if not utils.file_info(info.local_repo) then
|
||||
info.local_repo = false
|
||||
msg.warn("local repo not found - falling back to git")
|
||||
end
|
||||
end
|
||||
|
||||
if not info.local_repo then
|
||||
run({"git", "-C", info.edist, "remote", "add", "manager", info.git})
|
||||
run({"git", "-C", info.edist, "remote", "set-url", "manager", info.git})
|
||||
run({"git", "-C", info.edist, "fetch", "manager", info.branch})
|
||||
end
|
||||
|
||||
for file in string.gmatch(get_file_list(info), "[^\r\n]+") do
|
||||
|
||||
run({"git", "-C", e_dest, "remote", "add", "manager", info.git})
|
||||
run({"git", "-C", e_dest, "remote", "set-url", "manager", info.git})
|
||||
run({"git", "-C", e_dest, "fetch", "manager", info.branch})
|
||||
|
||||
for file in string.gmatch(run({"git", "-C", e_dest, "ls-tree", "-r", "--name-only", "remotes/manager/"..info.branch}).stdout, "[^\r\n]+") do
|
||||
local l_file = string.lower(file)
|
||||
if info.whitelist == "" or match(l_file, info.whitelist) then
|
||||
if info.blacklist == "" or not match(l_file, info.blacklist) then
|
||||
@@ -107,29 +73,20 @@ function update(info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if base == nil then return false end
|
||||
|
||||
|
||||
if base ~= "" then base = base.."/" end
|
||||
|
||||
|
||||
if next(files) == nil then
|
||||
print("no files matching patterns")
|
||||
else
|
||||
for _, file in ipairs(files) do
|
||||
local based = string.sub(file, string.len(base)+1)
|
||||
local p_based = parent(based)
|
||||
if p_based and not info.flatten_folders then mkdir(info.edist.."/"..p_based) end
|
||||
|
||||
local c = ""
|
||||
if info.local_repo then
|
||||
local source = io.open(info.local_repo..'/'..file)
|
||||
c = source:read("*a")
|
||||
source:close()
|
||||
else
|
||||
c = string.match(run({"git", "-C", info.edist, "--no-pager", "show", "remotes/manager/"..info.branch..":"..file}).stdout, "(.-)[\r\n]?$")
|
||||
end
|
||||
|
||||
local f = io.open(info.edist.."/"..(info.flatten_folders and file:match("[^/]+$") or based), "w")
|
||||
if p_based and not info.flatten_folders then mkdir(e_dest.."/"..p_based) end
|
||||
local c = string.match(run({"git", "-C", e_dest, "--no-pager", "show", "remotes/manager/"..info.branch..":"..file}).stdout, "(.-)[\r\n]?$")
|
||||
local f = io.open(e_dest.."/"..(info.flatten_folders and file:match("[^/]+$") or based), "w")
|
||||
f:write(c)
|
||||
f:close()
|
||||
end
|
||||
@@ -150,10 +107,8 @@ function update_all()
|
||||
end
|
||||
|
||||
for i, info in ipairs(config) do
|
||||
print("updating", (info.git:match("([^/]+)%.git$") or info.git).."...")
|
||||
if not update(info) then msg.error("FAILED") end
|
||||
print("update"..i, update(info))
|
||||
end
|
||||
print("all files updated")
|
||||
end
|
||||
|
||||
mp.add_key_binding(nil, "manager-update-all", update_all)
|
||||
mp.add_key_binding(nil, "manager-update-all", update_all)
|
||||
@@ -0,0 +1,224 @@
|
||||
-- Install [Torrserver](https://github.com/YouROK/TorrServer)
|
||||
-- then add "script-opts-append=mpv_torrserver-server=http://[TorrServer ip]:[port]" to mpv.conf
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
local opts = {
|
||||
server = "http://localhost:8090",
|
||||
torrserver_init = false,
|
||||
torrserver_path = "TorrServer",
|
||||
search_for_external_tracks = true
|
||||
}
|
||||
|
||||
(require 'mp.options').read_options(opts)
|
||||
local luacurl_available, cURL = pcall(require, 'cURL')
|
||||
|
||||
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
|
||||
|
||||
local function find_executable(name)
|
||||
local os_path = os.getenv("PATH") or ""
|
||||
local fallback_path = utils.join_path("/usr/bin", name)
|
||||
local exec_path
|
||||
for path in os_path:gmatch("[^:]+") do
|
||||
exec_path = utils.join_path(path, name)
|
||||
local meta, meta_error = utils.file_info(exec_path)
|
||||
if meta and meta.is_file then
|
||||
return exec_path
|
||||
end
|
||||
end
|
||||
if not is_windows then return fallback_path end
|
||||
return name -- fallback to just the name, hoping it's in PATH
|
||||
end
|
||||
|
||||
local function init()
|
||||
local exec_path = find_executable(opts.torrserver_path)
|
||||
local windows_args = { 'powershell', '-NoProfile', '-Command', exec_path }
|
||||
local unix_args = { '/bin/bash', '-c', exec_path }
|
||||
local args = is_windows and windows_args or unix_args
|
||||
local res = mp.command_native_async({ name = "subprocess", capture_stdout = true, playback_only = false, args = args })
|
||||
if res.status == 0 then
|
||||
mp.msg.error("TorrServer failed to start: ")
|
||||
end
|
||||
end
|
||||
|
||||
local char_to_hex = function(c)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end
|
||||
|
||||
local function urlencode(url)
|
||||
if url == nil then
|
||||
return
|
||||
end
|
||||
url = url:gsub("\n", "\r\n")
|
||||
url = url:gsub("([^%w ])", char_to_hex)
|
||||
url = url:gsub(" ", "+")
|
||||
return url
|
||||
end
|
||||
|
||||
local function get_magnet_info(url)
|
||||
local info_url = opts.server .. "/stream?stat&link=" .. urlencode(url)
|
||||
local res
|
||||
if not (luacurl_available) then
|
||||
-- if Lua-cURL is not available on this system
|
||||
local curl_cmd = {
|
||||
"curl",
|
||||
"-L",
|
||||
"--silent",
|
||||
"--max-time", "10",
|
||||
info_url
|
||||
}
|
||||
local cmd = mp.command_native {
|
||||
name = "subprocess",
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
args = curl_cmd
|
||||
}
|
||||
res = cmd.stdout
|
||||
else
|
||||
-- otherwise use Lua-cURL (binding to libcurl)
|
||||
local buf = {}
|
||||
local c = cURL.easy_init()
|
||||
c:setopt_followlocation(1)
|
||||
c:setopt_url(info_url)
|
||||
c:setopt_writefunction(function(chunk)
|
||||
table.insert(buf, chunk);
|
||||
return true;
|
||||
end)
|
||||
c:perform()
|
||||
res = table.concat(buf)
|
||||
end
|
||||
if res and res ~= "" then
|
||||
return (require 'mp.utils').parse_json(res)
|
||||
else
|
||||
return nil, "no info response (timeout?)"
|
||||
end
|
||||
end
|
||||
|
||||
local function edlencode(url)
|
||||
return "%" .. string.len(url) .. "%" .. url
|
||||
end
|
||||
|
||||
local function guess_type_by_extension(ext)
|
||||
if ext == "mkv" or ext == "mp4" or ext == "avi" or ext == "wmv" or ext == "vob" or ext == "m2ts" or ext == "ogm" then
|
||||
return "video"
|
||||
end
|
||||
if ext == "mka" or ext == "mp3" or ext == "aac" or ext == "flac" or ext == "ogg" or ext == "wma" or ext == "mpg"
|
||||
or ext == "wav" or ext == "wv" or ext == "opus" or ext == "ac3" then
|
||||
return "audio"
|
||||
end
|
||||
if ext == "ass" or ext == "srt" or ext == "vtt" then
|
||||
return "sub"
|
||||
end
|
||||
return "other";
|
||||
end
|
||||
|
||||
local function string_replace(str, match, replace)
|
||||
local s, e = string.find(str, match, 1, true)
|
||||
if s == nil or e == nil then
|
||||
return str
|
||||
end
|
||||
return string.sub(str, 1, s - 1) .. replace .. string.sub(str, e + 1)
|
||||
end
|
||||
|
||||
-- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst
|
||||
local function generate_m3u(magnet_uri, files)
|
||||
for _, fileinfo in ipairs(files) do
|
||||
-- strip top directory
|
||||
if fileinfo.path:find("/", 1, true) then
|
||||
fileinfo.fullpath = string.sub(fileinfo.path, fileinfo.path:find("/", 1, true) + 1)
|
||||
else
|
||||
fileinfo.fullpath = fileinfo.path
|
||||
end
|
||||
fileinfo.path = {}
|
||||
for w in fileinfo.fullpath:gmatch("([^/]+)") do table.insert(fileinfo.path, w) end
|
||||
local ext = string.match(fileinfo.path[#fileinfo.path], "%.(%w+)$")
|
||||
fileinfo.type = guess_type_by_extension(ext)
|
||||
end
|
||||
table.sort(files, function(a, b)
|
||||
-- make top-level files appear first in the playlist
|
||||
if (#a.path == 1 or #b.path == 1) and #a.path ~= #b.path then
|
||||
return #a.path < #b.path
|
||||
end
|
||||
-- make videos first
|
||||
if (a.type == "video" or b.type == "video") and a.type ~= b.type then
|
||||
return a.type == "video"
|
||||
end
|
||||
-- otherwise sort by path
|
||||
return a.fullpath < b.fullpath
|
||||
end);
|
||||
|
||||
local infohash = magnet_uri:match("^magnet:%?xt=urn:bt[im]h:(%w+)") or urlencode(magnet_uri)
|
||||
|
||||
local playlist = { '#EXTM3U' }
|
||||
|
||||
for _, fileinfo in ipairs(files) do
|
||||
if fileinfo.processed ~= true then
|
||||
table.insert(playlist, '#EXTINF:0,' .. fileinfo.fullpath)
|
||||
local basename = string.match(fileinfo.path[#fileinfo.path], '^(.+)%.%w+$')
|
||||
|
||||
local url = opts.server .. "/stream/" .. urlencode(fileinfo.fullpath) .."?play&index=" .. fileinfo.id .. "&link=" .. infohash
|
||||
local hdr = { "!new_stream", "!no_clip",
|
||||
--"!track_meta,title=" .. edlencode(basename),
|
||||
edlencode(url)
|
||||
}
|
||||
local edl = "edl://" .. table.concat(hdr, ";") .. ";"
|
||||
local external_tracks = 0
|
||||
|
||||
fileinfo.processed = true
|
||||
if opts.search_for_external_tracks and basename ~= nil and fileinfo.type == "video" then
|
||||
mp.msg.info("!" .. basename)
|
||||
|
||||
for _, fileinfo2 in ipairs(files) do
|
||||
if #fileinfo2.path > 0 and
|
||||
fileinfo2.type ~= "other" and
|
||||
fileinfo2.processed ~= true and
|
||||
string.find(fileinfo2.path[#fileinfo2.path], basename, 1, true) ~= nil
|
||||
then
|
||||
mp.msg.info("->" .. fileinfo2.fullpath)
|
||||
local title = string_replace(fileinfo2.fullpath, basename, "%")
|
||||
local url = opts.server .. "/stream/" .. urlencode(fileinfo2.fullpath).."?play&index=" .. fileinfo2.id .. "&link=" .. infohash
|
||||
local hdr = { "!new_stream", "!no_clip", "!no_chapters",
|
||||
"!delay_open,media_type=" .. fileinfo2.type,
|
||||
"!track_meta,title=" .. edlencode(title),
|
||||
edlencode(url)
|
||||
}
|
||||
edl = edl .. table.concat(hdr, ";") .. ";"
|
||||
fileinfo2.processed = true
|
||||
external_tracks = external_tracks + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if external_tracks == 0 then -- dont use edl
|
||||
table.insert(playlist, url)
|
||||
else
|
||||
table.insert(playlist, edl)
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(playlist, '\n')
|
||||
end
|
||||
|
||||
mp.add_hook("on_load", 5, function()
|
||||
local url = mp.get_property("stream-open-filename")
|
||||
if url:find("^magnet:") == 1 or (url:find("^https?://") == 1 and url:find("%.torrent$") ~= nil) then
|
||||
mp.set_property_bool("file-local-options/ytdl", false)
|
||||
if opts.torrserver_init then init() end
|
||||
local magnet_info, err = get_magnet_info(url)
|
||||
if type(magnet_info) == "table" then
|
||||
if magnet_info.file_stats then
|
||||
-- torrent has multiple files. open as playlist
|
||||
mp.set_property("stream-open-filename", "memory://" .. generate_m3u(url, magnet_info.file_stats))
|
||||
return
|
||||
end
|
||||
-- if not a playlist and has a name
|
||||
if magnet_info.name then
|
||||
mp.set_property("stream-open-filename", "memory://#EXTM3U\n" ..
|
||||
"#EXTINF:0," .. magnet_info.name .. "\n" ..
|
||||
opts.server .. "/stream?play&index=1&link=" .. urlencode(url))
|
||||
return
|
||||
end
|
||||
else
|
||||
mp.msg.warn("error: " .. err)
|
||||
end
|
||||
mp.set_property("stream-open-filename", opts.server .. "/stream?m3u&link=" .. urlencode(url))
|
||||
end
|
||||
end)
|
||||
@@ -402,4 +402,4 @@
|
||||
"zui": "最嘴罪醉咀蕞䮔厜璻蟕晬嗺噿嶵㠑嶊冣㝡䘹祽鋷錊酻酔樶檌㰎栬槜檇辠䘒稡纗絊",
|
||||
"zun": "尊遵樽鳟撙墫噂嶟鶎銌鱒鐏捘罇鷷僔繜譐",
|
||||
"zuo": "作做坐左座昨佐琢撮柞唑祚捽阼胙嘬怍酢笮葄葃蓙䔘苲莋㸲㝾䞰䎰咗㘀㘴岝岞䝫糳袏鈼㭮稓穝秨筰㛗㑅飵侳繓䋏"
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,4 @@ function BufferingIndicator:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return BufferingIndicator
|
||||
return BufferingIndicator
|
||||
@@ -40,7 +40,7 @@ function Button:render()
|
||||
|
||||
local ass = assdraw.ass_new()
|
||||
local is_clickable = self.is_clickable and self.on_click ~= nil
|
||||
local is_hover = self.proximity_raw == 0
|
||||
local is_hover = self.proximity_raw <= 0
|
||||
local foreground = self.active and self.background or self.foreground
|
||||
local background = self.active and self.foreground or self.background
|
||||
local background_opacity = self.active and 1 or config.opacity.controls
|
||||
@@ -97,4 +97,4 @@ function Button:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Button
|
||||
return Button
|
||||
@@ -37,17 +37,17 @@ function Controls:init_options()
|
||||
-- Serialize control elements
|
||||
local shorthands = {
|
||||
['play-pause'] = 'cycle:pause:pause:no/yes=play_arrow?' .. t('Play/Pause'),
|
||||
menu = 'command:menu_book:script-binding uosc/menu-blurred?' .. t('Menu'),
|
||||
subtitles = 'command:closed_caption:script-binding uosc/subtitles#sub>1?' .. t('Subtitles'),
|
||||
menu = 'command:menu:script-binding uosc/menu-blurred?' .. t('Menu'),
|
||||
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?' .. t('Subtitles'),
|
||||
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?' .. t('Audio'),
|
||||
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?' .. t('Audio device'),
|
||||
video = 'command:smart_display:script-binding uosc/video#video>1?' .. t('Video'),
|
||||
playlist = 'command:list_alt:script-binding uosc/playlist#playlist>1?' .. t('Playlist'),
|
||||
chapters = 'command:library_books:script-binding uosc/chapters#chapters>1?' .. t('Chapters'),
|
||||
['editions'] = 'command:movie_filter:script-binding uosc/editions#editions>1?' .. t('Editions'),
|
||||
video = 'command:theaters:script-binding uosc/video#video>1?' .. t('Video'),
|
||||
playlist = 'command:list_alt:script-binding uosc/playlist?' .. t('Playlist'),
|
||||
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?' .. t('Chapters'),
|
||||
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?' .. t('Editions'),
|
||||
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?' .. t('Stream quality'),
|
||||
['open-file'] = 'command:folder:script-binding uosc/open-file?' .. t('Open file'),
|
||||
['items'] = 'command:list_alt:script-binding uosc/items#playlist>1?' .. t('Playlist/Files'),
|
||||
['open-file'] = 'command:file_open:script-binding uosc/open-file?' .. t('Open file'),
|
||||
['items'] = 'command:list_alt:script-binding uosc/items?' .. t('Playlist/Files'),
|
||||
prev = 'command:arrow_back_ios:script-binding uosc/prev?' .. t('Previous'),
|
||||
next = 'command:arrow_forward_ios:script-binding uosc/next?' .. t('Next'),
|
||||
first = 'command:first_page:script-binding uosc/first?' .. t('First'),
|
||||
@@ -271,9 +271,6 @@ function Controls:register_badge_updater(badge, element)
|
||||
for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end
|
||||
return count
|
||||
end
|
||||
elseif prop == 'playlist' then
|
||||
observable_name = 'playlist-count'
|
||||
serializer = function(count) return count end
|
||||
else
|
||||
local parts = split(prop, '@')
|
||||
-- Support both new `prop@owner` and old `@prop` syntaxes
|
||||
@@ -428,4 +425,4 @@ function Controls:on_options()
|
||||
self:init_options()
|
||||
end
|
||||
|
||||
return Controls
|
||||
return Controls
|
||||
@@ -32,4 +32,4 @@ function Curtain:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Curtain
|
||||
return Curtain
|
||||
@@ -83,4 +83,4 @@ function CycleButton:init(id, props)
|
||||
end
|
||||
end
|
||||
|
||||
return CycleButton
|
||||
return CycleButton
|
||||
@@ -262,4 +262,4 @@ function Element:create_action(fn)
|
||||
end
|
||||
end
|
||||
|
||||
return Element
|
||||
return Element
|
||||
@@ -48,14 +48,14 @@ function Elements:update_proximities()
|
||||
element:update_proximity()
|
||||
end
|
||||
|
||||
if element.proximity_raw == 0 then
|
||||
if element.proximity_raw <= 0 then
|
||||
-- Mouse entered element area
|
||||
if previous_proximity_raw ~= 0 then
|
||||
if previous_proximity_raw > 0 then
|
||||
mouse_enter_elements[#mouse_enter_elements + 1] = element
|
||||
end
|
||||
else
|
||||
-- Mouse left element area
|
||||
if previous_proximity_raw == 0 then
|
||||
if previous_proximity_raw <= 0 then
|
||||
mouse_leave_elements[#mouse_leave_elements + 1] = element
|
||||
end
|
||||
end
|
||||
@@ -122,7 +122,7 @@ function Elements:proximity_trigger(name, ...)
|
||||
for i = #self._all, 1, -1 do
|
||||
local element = self._all[i]
|
||||
if element.enabled then
|
||||
if element.proximity_raw == 0 then
|
||||
if element.proximity_raw <= 0 then
|
||||
if element:trigger(name, ...) == 'stop_propagation' then break end
|
||||
end
|
||||
if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end
|
||||
@@ -149,4 +149,4 @@ end
|
||||
function Elements:has(id) return self[id] ~= nil end
|
||||
function Elements:ipairs() return ipairs(self._all) end
|
||||
|
||||
return Elements
|
||||
return Elements
|
||||
@@ -33,4 +33,4 @@ function ManagedButton:update(data)
|
||||
end
|
||||
end
|
||||
|
||||
return ManagedButton
|
||||
return ManagedButton
|
||||
+110
-118
@@ -309,7 +309,7 @@ function Menu:update_content_dimensions()
|
||||
|
||||
for _, menu in ipairs(self.all) do
|
||||
title_opts.bold, title_opts.italic = true, false
|
||||
local max_width = text_width(menu.title, title_opts) + 2 * self.padding + 2 * self.item_padding
|
||||
local max_width = text_width(menu.title, title_opts) + 2 * self.item_padding
|
||||
|
||||
-- Estimate width of a widest item
|
||||
for _, item in ipairs(menu.items) do
|
||||
@@ -323,7 +323,7 @@ function Menu:update_content_dimensions()
|
||||
if estimated_width > max_width then max_width = estimated_width end
|
||||
end
|
||||
|
||||
menu.max_width = max_width + 2 * self.padding
|
||||
menu.max_width = max_width
|
||||
end
|
||||
|
||||
self:update_dimensions()
|
||||
@@ -336,20 +336,21 @@ function Menu:update_dimensions()
|
||||
-- and dumb titles with no search inputs. It could use a refactor.
|
||||
local margin = round(self.item_height / 2)
|
||||
local external_buttons_reserve = display.width / self.item_height > 14 and self.scroll_step * 6 - margin * 2 or 0
|
||||
local width_available = display.width - margin * 2 - external_buttons_reserve
|
||||
local height_available = display.height - margin * 2
|
||||
local width_available = display.width - margin * 2 - self.padding * 2 - external_buttons_reserve
|
||||
local height_available = display.height - margin * 2 - self.padding * 2
|
||||
local min_width = math.min(self.min_width, width_available)
|
||||
|
||||
for _, menu in ipairs(self.all) do
|
||||
local width = math.max(menu.search and menu.search.max_width or 0, menu.max_width)
|
||||
menu.width = round(clamp(min_width, width, width_available))
|
||||
local title_height = (menu.is_root and menu.title or menu.search) and self.scroll_step + self.padding or 0
|
||||
local title_height = (menu.is_root and menu.title or menu.search) and
|
||||
self.scroll_step + self.separator_size + 1 or 0
|
||||
local footnote_height = self.font_size * 1.5
|
||||
local max_height = height_available - title_height - footnote_height
|
||||
local content_height = self.scroll_step * #menu.items
|
||||
menu.height = math.min(content_height - self.item_spacing, max_height)
|
||||
menu.top = clamp(
|
||||
title_height + margin,
|
||||
title_height + margin + self.padding,
|
||||
menu.search and math.min(menu.search.min_top, menu.search.source.top) or height_available,
|
||||
round((height_available - menu.height + title_height) / 2)
|
||||
)
|
||||
@@ -364,10 +365,13 @@ function Menu:update_dimensions()
|
||||
self:update_coordinates()
|
||||
end
|
||||
|
||||
-- Updates element coordinates to match currently open (sub)menu.
|
||||
-- Updates element coordinates to match padding box of currently open (sub)menu.
|
||||
function Menu:update_coordinates()
|
||||
local ax = round((display.width - self.current.width) / 2) + self.offset_x
|
||||
self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height)
|
||||
local ax = round((display.width - self.current.width) / 2 - self.padding) + self.offset_x
|
||||
self:set_coordinates(
|
||||
ax, self.current.top - self.padding,
|
||||
ax + self.current.width + self.padding * 2, self.current.top + self.current.height + self.padding
|
||||
)
|
||||
end
|
||||
|
||||
function Menu:reset_navigation()
|
||||
@@ -686,7 +690,7 @@ function Menu:on_prop_fullormaxed() self:update_content_dimensions() end
|
||||
function Menu:on_options() self:update_content_dimensions() end
|
||||
|
||||
function Menu:handle_cursor_down()
|
||||
if self.proximity_raw == 0 then
|
||||
if self.proximity_raw <= 0 then
|
||||
self.drag_last_y = cursor.y
|
||||
self.current.fling = nil
|
||||
else
|
||||
@@ -696,7 +700,7 @@ end
|
||||
|
||||
---@param shortcut? Shortcut
|
||||
function Menu:handle_cursor_up(shortcut)
|
||||
if self.proximity_raw == 0 and self.drag_last_y and not self.is_dragging then
|
||||
if self.proximity_raw <= -self.padding and self.drag_last_y and not self.is_dragging then
|
||||
self:activate_selected_item(shortcut, true)
|
||||
end
|
||||
if self.is_dragging then
|
||||
@@ -893,14 +897,14 @@ function search_items(items, query, recursive, prefix)
|
||||
if ligature_conv_title:find(query, 1, true) then
|
||||
match = true
|
||||
score = 1000
|
||||
local pos = get_roman_match_positions(title, query, "ligature", ligature_roman)
|
||||
local pos = get_roman_match_positions(title, query, 'ligature', ligature_roman)
|
||||
if pos then
|
||||
ass_safe_title = highlight_match(item.title, pos, font_color, bold)
|
||||
end
|
||||
elseif initials_conv_title:find(query, 1, true) then
|
||||
match = true
|
||||
score = 900
|
||||
local pos = get_roman_match_positions(title, query, "initial", initials_roman)
|
||||
local pos = get_roman_match_positions(title, query, 'initial', initials_roman)
|
||||
if pos then
|
||||
ass_safe_title = highlight_match(item.title, pos, font_color, bold)
|
||||
end
|
||||
@@ -1371,7 +1375,6 @@ function Menu:render()
|
||||
cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
|
||||
|
||||
local ass = assdraw.ass_new()
|
||||
local spacing = self.item_padding
|
||||
local icon_size = self.font_size
|
||||
|
||||
---@param menu MenuStack
|
||||
@@ -1380,37 +1383,43 @@ function Menu:render()
|
||||
local function draw_menu(menu, x, pos)
|
||||
local is_current, is_parent, is_submenu = pos == 0, pos < 0, pos > 0
|
||||
local menu_opacity = (pos == 0 and 1 or config.opacity.submenu ^ math.abs(pos)) * self.opacity
|
||||
local ax, ay, bx, by = x, menu.top, x + menu.width, menu.top + menu.height
|
||||
-- Scrollable content area coordinates
|
||||
local content_rect = {
|
||||
ax = x + self.padding,
|
||||
ay = menu.top,
|
||||
bx = x + self.padding + menu.width,
|
||||
by = menu.top + menu.height,
|
||||
}
|
||||
-- local ax, ay, bx, by = x + self.padding, menu.top, x + menu.width + self.padding, menu.top + menu.height
|
||||
local draw_title = menu.is_root and menu.title or menu.search
|
||||
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
|
||||
local scroll_clip = '\\clip(0,' .. content_rect.ay .. ',' .. display.width .. ',' .. content_rect.by .. ')'
|
||||
local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
|
||||
local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
|
||||
local menu_rect = {
|
||||
ax = ax,
|
||||
ay = ay - (draw_title and self.scroll_step + self.padding or 0) - self.padding,
|
||||
bx = bx,
|
||||
by = by + self.padding,
|
||||
local bg_rect = {
|
||||
ax = x,
|
||||
ay = content_rect.ay - (draw_title and self.scroll_step or 0) - self.padding,
|
||||
bx = content_rect.bx + self.padding,
|
||||
by = content_rect.by + self.padding,
|
||||
}
|
||||
local blur_selected_index = self.mouse_nav and is_current
|
||||
local blur_action_index = self.mouse_nav and menu.action_index ~= nil
|
||||
|
||||
-- Background
|
||||
ass:rect(menu_rect.ax, menu_rect.ay, menu_rect.bx, menu_rect.by, {
|
||||
ass:rect(bg_rect.ax, bg_rect.ay, bg_rect.bx, bg_rect.by, {
|
||||
color = bg,
|
||||
opacity = menu_opacity * config.opacity.menu,
|
||||
radius = state.radius > 0 and state.radius + self.padding or 0,
|
||||
radius = state.radius > 0 and math.min(state.radius + self.padding, state.radius * 3) or 0,
|
||||
})
|
||||
|
||||
if is_parent then
|
||||
cursor:zone('primary_down', menu_rect, self:create_action(function() self:slide_in_menu(menu.id, x) end))
|
||||
cursor:zone('primary_down', bg_rect, self:create_action(function() self:slide_in_menu(menu.id, x) end))
|
||||
end
|
||||
|
||||
-- Scrollbar
|
||||
if menu.scroll_height > 0 then
|
||||
local groove_height = menu.height - 2
|
||||
local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40)
|
||||
local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
|
||||
local sax = bx - round(self.scrollbar_size / 2)
|
||||
local thumb_y = content_rect.ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height))
|
||||
local sax = content_rect.bx - round(self.scrollbar_size / 2)
|
||||
local sbx = sax + self.scrollbar_size
|
||||
ass:rect(sax, thumb_y, sbx, thumb_y + thumb_height, {color = fg, opacity = menu_opacity * 0.8})
|
||||
end
|
||||
@@ -1419,7 +1428,7 @@ function Menu:render()
|
||||
local submenu_rect, current_item = nil, is_current and menu.selected_index and menu.items[menu.selected_index]
|
||||
local submenu_is_hovered = false
|
||||
if current_item and current_item.items then
|
||||
submenu_rect = draw_menu(current_item --[[@as MenuStack]], menu_rect.bx + self.gap, 1)
|
||||
submenu_rect = draw_menu(current_item --[[@as MenuStack]], bg_rect.bx + self.gap, 1)
|
||||
cursor:zone('primary_down', submenu_rect, self:create_action(function(shortcut)
|
||||
self:activate_selected_item(shortcut, true)
|
||||
end))
|
||||
@@ -1432,21 +1441,32 @@ function Menu:render()
|
||||
|
||||
if not item then break end
|
||||
|
||||
local item_ax = menu_rect.ax + self.padding
|
||||
local item_bx = menu_rect.bx - self.padding
|
||||
local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1)
|
||||
local item_ay = content_rect.ay - menu.scroll_y + self.scroll_step * (index - 1)
|
||||
local item_by = item_ay + self.item_height
|
||||
local item_center_y = item_ay + (self.item_height / 2)
|
||||
local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil
|
||||
local content_ax, content_bx = ax + self.padding + spacing, bx - self.padding - spacing
|
||||
local item_clip = (item_ay < content_rect.ay or item_by > content_rect.by) and scroll_clip or nil
|
||||
local content_ax, content_bx = content_rect.ax + self.item_padding,
|
||||
content_rect.bx - self.item_padding
|
||||
local is_selected = menu.selected_index == index
|
||||
local item_rect_hitbox = {
|
||||
ax = item_ax,
|
||||
ay = math.max(item_ay, menu_rect.ay),
|
||||
bx = menu_rect.bx + (item.items and self.gap or -self.padding), -- to bridge the gap with cursor
|
||||
by = math.min(item_ay + self.scroll_step, menu_rect.by),
|
||||
ax = content_rect.ax,
|
||||
ay = math.max(item_ay, bg_rect.ay),
|
||||
bx = bg_rect.bx + (item.items and self.gap or -self.padding), -- to bridge the submenu gap with cursor
|
||||
by = math.min(item_ay + self.scroll_step, bg_rect.by),
|
||||
}
|
||||
|
||||
-- Select hovered item
|
||||
if is_current and self.mouse_nav and item.selectable ~= false
|
||||
-- Do not select items if cursor is moving towards a submenu
|
||||
and (not submenu_rect or not cursor:direction_to_rectangle_distance(submenu_rect))
|
||||
and (submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) <= 0) then
|
||||
menu.selected_index = index
|
||||
if not is_selected then
|
||||
is_selected = true
|
||||
request_render()
|
||||
end
|
||||
end
|
||||
|
||||
local has_background = is_selected or item.active
|
||||
local next_item = menu.items[index + 1]
|
||||
local next_is_active = next_item and next_item.active
|
||||
@@ -1458,22 +1478,23 @@ function Menu:render()
|
||||
if action then selected_action = action end
|
||||
|
||||
-- Separator
|
||||
if item_by < by and ((not has_background and not next_has_background) or item.separator) then
|
||||
local separator_ay, separator_by = item_by, item_by + self.separator_size
|
||||
if item_by < content_rect.by and ((not has_background and not next_has_background) or item.separator) then
|
||||
local ay, by = item_by, item_by + self.separator_size
|
||||
if has_background then
|
||||
separator_ay, separator_by = separator_ay + self.separator_size, separator_by + self.separator_size
|
||||
ay, by = ay + self.separator_size, by + self.separator_size
|
||||
elseif next_has_background then
|
||||
separator_ay, separator_by = separator_ay - self.separator_size, separator_by - self.separator_size
|
||||
ay, by = ay - self.separator_size, by - self.separator_size
|
||||
end
|
||||
ass:rect(ax + spacing, separator_ay, bx - spacing, separator_by, {
|
||||
color = fg, opacity = menu_opacity * (item.separator and 0.13 or 0.04),
|
||||
})
|
||||
ass:rect(
|
||||
content_rect.ax + self.item_padding, ay, content_rect.bx - self.item_padding, by,
|
||||
{color = fg, opacity = menu_opacity * (item.separator and 0.13 or 0.04)}
|
||||
)
|
||||
end
|
||||
|
||||
-- Background
|
||||
local highlight_opacity = 0 + (item.active and 0.8 or 0) + (is_selected and 0.15 or 0)
|
||||
if highlight_opacity > 0 then
|
||||
ass:rect(ax + self.padding, item_ay, bx - self.padding, item_by, {
|
||||
ass:rect(content_rect.ax, item_ay, content_rect.bx, item_by, {
|
||||
radius = state.radius,
|
||||
color = fg,
|
||||
opacity = highlight_opacity * menu_opacity,
|
||||
@@ -1495,9 +1516,10 @@ function Menu:render()
|
||||
actions_rect = {
|
||||
ay = item_ay + margin,
|
||||
by = item_by - margin,
|
||||
is_outside = place == 'outside' and display.width - menu_rect.bx + margin * 2 > rect_width,
|
||||
is_outside = place == 'outside' and display.width - bg_rect.bx + margin * 2 > rect_width,
|
||||
}
|
||||
actions_rect.bx = actions_rect.is_outside and menu_rect.bx + margin + rect_width or item_bx - margin
|
||||
actions_rect.bx = actions_rect.is_outside and bg_rect.bx + margin + rect_width or
|
||||
content_rect.bx - margin
|
||||
actions_rect.ax = actions_rect.bx
|
||||
|
||||
for i = 1, #actions, 1 do
|
||||
@@ -1532,7 +1554,7 @@ function Menu:render()
|
||||
rect.ay, rect.by, rect.bx = item_ay, item_ay + self.scroll_step, rect.bx + margin
|
||||
|
||||
-- Select action on cursor hover
|
||||
if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) == 0 then
|
||||
if self.mouse_nav and get_point_to_rectangle_proximity(cursor, rect) <= 0 then
|
||||
cursor:zone('primary_down', rect, self:create_action(function(shortcut)
|
||||
self:activate_selected_item(shortcut, true)
|
||||
end))
|
||||
@@ -1553,17 +1575,18 @@ function Menu:render()
|
||||
if is_selected and not selected_action then
|
||||
local size = round(2 * state.scale)
|
||||
local v_padding = math.min(state.radius, math.ceil(self.item_height / 3))
|
||||
ass:rect(ax + self.padding - size - 1, item_ay + v_padding, ax + self.padding - 1,
|
||||
item_by - v_padding, {
|
||||
radius = 1 * state.scale, color = fg, opacity = menu_opacity, clip = item_clip,
|
||||
})
|
||||
ass:rect(
|
||||
content_rect.ax - size - 1, item_ay + v_padding,
|
||||
content_rect.ax - 1, item_by - v_padding,
|
||||
{radius = 1 * state.scale, color = fg, opacity = menu_opacity, clip = item_clip}
|
||||
)
|
||||
end
|
||||
|
||||
-- Icon
|
||||
if item.icon then
|
||||
if not actions_rect or actions_rect.is_outside then
|
||||
local x = (not item.title and not item.hint and item.align == 'center')
|
||||
and menu_rect.ax + (menu_rect.bx - menu_rect.ax) / 2
|
||||
and bg_rect.ax + (bg_rect.bx - bg_rect.ax) / 2
|
||||
or content_bx - (icon_size / 2)
|
||||
if item.icon == 'spinner' then
|
||||
ass:spinner(x, item_center_y, icon_size * 1.5, {color = font_color, opacity = menu_opacity * 0.8})
|
||||
@@ -1573,7 +1596,7 @@ function Menu:render()
|
||||
})
|
||||
end
|
||||
end
|
||||
content_bx = content_bx - icon_size - spacing
|
||||
content_bx = content_bx - icon_size - self.item_padding
|
||||
title_clip_bx = math.min(content_bx, title_clip_bx)
|
||||
end
|
||||
|
||||
@@ -1581,7 +1604,7 @@ function Menu:render()
|
||||
if item.hint_width > 0 then
|
||||
-- controls title & hint clipping proportional to the ratio of their widths
|
||||
-- both title and hint get at least 50% of the width, unless they are smaller then that
|
||||
local width = content_bx - content_ax - spacing
|
||||
local width = content_bx - content_ax - self.item_padding
|
||||
local title_min = math.min(item.title_width, width * 0.5)
|
||||
local hint_min = math.min(item.hint_width, width * 0.5)
|
||||
local title_ratio = item.title_width / (item.title_width + item.hint_width)
|
||||
@@ -1594,8 +1617,9 @@ function Menu:render()
|
||||
-- Hint
|
||||
if item.hint then
|
||||
item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint)
|
||||
local clip = '\\clip(' .. title_clip_bx + spacing .. ',' ..
|
||||
math.max(item_ay, ay) .. ',' .. hint_clip_bx .. ',' .. math.min(item_by, by) .. ')'
|
||||
local clip = '\\clip(' .. title_clip_bx + self.item_padding .. ','
|
||||
.. math.max(item_ay, content_rect.ay) .. ',' .. hint_clip_bx .. ','
|
||||
.. math.min(item_by, content_rect.by) .. ')'
|
||||
ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, {
|
||||
size = self.font_size_hint,
|
||||
color = font_color,
|
||||
@@ -1608,8 +1632,8 @@ function Menu:render()
|
||||
-- Title
|
||||
if item.title then
|
||||
item.ass_safe_title = item.ass_safe_title or ass_escape(item.title)
|
||||
local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ','
|
||||
.. title_clip_bx .. ',' .. math.min(item_by, by) .. ')'
|
||||
local clip = '\\clip(' .. content_rect.ax .. ',' .. math.max(item_ay, content_rect.ay) .. ','
|
||||
.. title_clip_bx .. ',' .. math.min(item_by, content_rect.by) .. ')'
|
||||
local title_x, align = content_ax, 4
|
||||
if item.align == 'right' then
|
||||
title_x, align = title_clip_bx, 6
|
||||
@@ -1626,29 +1650,12 @@ function Menu:render()
|
||||
clip = clip,
|
||||
})
|
||||
end
|
||||
|
||||
-- Select hovered item
|
||||
if is_current and self.mouse_nav and item.selectable ~= false then
|
||||
if submenu_rect and cursor:direction_to_rectangle_distance(submenu_rect)
|
||||
or actions_rect and actions_rect.is_outside and cursor:direction_to_rectangle_distance(actions_rect) then
|
||||
blur_selected_index = false
|
||||
else
|
||||
if submenu_is_hovered or get_point_to_rectangle_proximity(cursor, item_rect_hitbox) == 0 then
|
||||
blur_selected_index = false
|
||||
menu.selected_index = index
|
||||
if not is_selected then
|
||||
is_selected = true
|
||||
request_render()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Footnote / Selected action label
|
||||
if is_current and (menu.footnote or selected_action) then
|
||||
local height_half = self.font_size
|
||||
local icon_x, icon_y = menu_rect.ax + self.padding + self.font_size / 2, menu_rect.by + height_half
|
||||
local icon_x, icon_y = content_rect.ax + self.font_size / 2, bg_rect.by + height_half
|
||||
local is_icon_hovered = false
|
||||
local icon_hitbox = {
|
||||
ax = icon_x - height_half,
|
||||
@@ -1656,14 +1663,14 @@ function Menu:render()
|
||||
bx = icon_x + height_half,
|
||||
by = icon_y + height_half,
|
||||
}
|
||||
is_icon_hovered = get_point_to_rectangle_proximity(cursor, icon_hitbox) == 0
|
||||
is_icon_hovered = get_point_to_rectangle_proximity(cursor, icon_hitbox) <= 0
|
||||
local text = selected_action and selected_action.label or is_icon_hovered and menu.footnote
|
||||
local opacity = (is_icon_hovered and 1 or 0.5) * menu_opacity
|
||||
ass:icon(icon_x, icon_y, self.font_size, is_icon_hovered and 'help' or 'help_outline', {
|
||||
color = fg, border = state.scale, border_color = bg, opacity = opacity,
|
||||
})
|
||||
if text then
|
||||
ass:txt(icon_x + self.font_size * 0.75, icon_y, 4, text, {
|
||||
ass:txt(icon_x + self.font_size * 0.75, icon_y - self.font_size * 0.5, 7, ass_escape(text), {
|
||||
size = self.font_size,
|
||||
color = fg,
|
||||
border = state.scale,
|
||||
@@ -1676,43 +1683,24 @@ function Menu:render()
|
||||
|
||||
-- Menu title
|
||||
if draw_title then
|
||||
local title_height = self.item_height + self.padding - 3
|
||||
local requires_submit = menu.search_debounce == 'submit'
|
||||
local rect = {
|
||||
ax = round(ax + spacing / 2 + self.padding),
|
||||
ay = ay - self.scroll_step - self.padding * 2,
|
||||
bx = round(bx - spacing / 2 - self.padding),
|
||||
by = math.min(by, ay - self.padding),
|
||||
ax = content_rect.ax,
|
||||
ay = content_rect.ay - self.scroll_step - self.separator_size - 1,
|
||||
bx = content_rect.bx,
|
||||
by = content_rect.ay - self.separator_size - 1,
|
||||
}
|
||||
-- centers
|
||||
-- Centers
|
||||
rect.cx, rect.cy = round(rect.ax + (rect.bx - rect.ax) / 2), round(rect.ay + (rect.by - rect.ay) / 2)
|
||||
|
||||
if menu.title and not menu.ass_safe_title then
|
||||
menu.ass_safe_title = ass_escape(menu.title)
|
||||
end
|
||||
|
||||
-- Background
|
||||
if menu.search then
|
||||
ass:rect(ax + 3, rect.ay + 3, bx - 3, rect.ay + title_height - 1, {
|
||||
color = fg .. '\\1a&HFF', opacity = menu_opacity * 0.1,
|
||||
radius = state.radius > 0 and state.radius + self.padding or 0,
|
||||
border = 1, border_color = fg, border_opacity = menu_opacity * 0.8
|
||||
})
|
||||
ass:texture(ax + 3, rect.ay + 3, bx - 3, rect.ay + title_height - 1, 'n', {
|
||||
size = 80, color = bg, opacity = menu_opacity * 0.1, anchor_x = ax + 2, anchor_y = rect.ay + 2,
|
||||
})
|
||||
else
|
||||
ass:rect(ax + 2, rect.ay + 2, bx - 2, rect.ay + title_height, {
|
||||
color = fg, opacity = menu_opacity * 0.8,
|
||||
radius = state.radius > 0 and state.radius + self.padding or 0,
|
||||
})
|
||||
ass:texture(ax + 2, rect.ay + 2, bx - 2, rect.ay + title_height, 'n', {
|
||||
size = 80, color = bg, opacity = menu_opacity * 0.1,
|
||||
})
|
||||
end
|
||||
|
||||
-- Bottom border
|
||||
ass:rect(ax, rect.by - self.separator_size, bx, rect.by, {color = fg, opacity = menu_opacity * 0.2})
|
||||
-- Separator
|
||||
ass:rect(
|
||||
rect.ax, rect.by, rect.bx, rect.by + self.separator_size, {color = fg, opacity = menu_opacity * 0.2}
|
||||
)
|
||||
|
||||
-- Blur selection (also activates search input) when user clicks title
|
||||
if is_current then
|
||||
@@ -1725,11 +1713,16 @@ function Menu:render()
|
||||
if menu.search then
|
||||
-- Icon
|
||||
local icon_size, icon_opacity = self.font_size * 1.3, menu_opacity * (requires_submit and 0.5 or 1)
|
||||
local icon_rect = {ax = rect.ax, ay = rect.ay, bx = ax + icon_size + spacing * 1.5, by = rect.by}
|
||||
local icon_rect = {
|
||||
ax = rect.ax,
|
||||
ay = rect.ay,
|
||||
bx = content_rect.ax + icon_size + self.item_padding * 1.5,
|
||||
by = rect.by,
|
||||
}
|
||||
|
||||
if is_current and requires_submit then
|
||||
cursor:zone('primary_down', icon_rect, function() self:search_submit() end)
|
||||
if get_point_to_rectangle_proximity(cursor, icon_rect) == 0 then
|
||||
if get_point_to_rectangle_proximity(cursor, icon_rect) <= 0 then
|
||||
icon_opacity = menu_opacity
|
||||
end
|
||||
end
|
||||
@@ -1778,7 +1771,10 @@ function Menu:render()
|
||||
-- (input is selected when `selected_index` is `nil`)
|
||||
if menu.search_debounce == 'submit' and not menu.selected_index then
|
||||
local size_half = round(1 * state.scale)
|
||||
ass:rect(ax, rect.by - size_half, bx, rect.by + size_half, {color = fg, opacity = menu_opacity})
|
||||
ass:rect(
|
||||
content_rect.ax, rect.by - size_half, content_rect.bx, rect.by + size_half,
|
||||
{color = fg, opacity = menu_opacity}
|
||||
)
|
||||
end
|
||||
local input_is_blurred = menu.search_debounce == 'submit' and menu.selected_index
|
||||
|
||||
@@ -1793,7 +1789,7 @@ function Menu:render()
|
||||
ass:txt(rect.cx, rect.cy, 5, menu.ass_safe_title, {
|
||||
size = self.font_size,
|
||||
bold = true,
|
||||
color = bg,
|
||||
color = bgt,
|
||||
wrap = 2,
|
||||
opacity = menu_opacity,
|
||||
clip = '\\clip(' .. rect.ax .. ',' .. rect.ay .. ',' .. rect.bx .. ',' .. rect.by .. ')',
|
||||
@@ -1801,16 +1797,12 @@ function Menu:render()
|
||||
end
|
||||
end
|
||||
|
||||
-- We are in mouse nav and cursor isn't hovering any item
|
||||
if blur_selected_index then
|
||||
menu.selected_index = nil
|
||||
end
|
||||
if blur_action_index then
|
||||
menu.action_index = nil
|
||||
request_render()
|
||||
end
|
||||
|
||||
return menu_rect
|
||||
return bg_rect
|
||||
end
|
||||
|
||||
-- Active menu
|
||||
@@ -1821,7 +1813,7 @@ function Menu:render()
|
||||
local parent_offset_x, parent_horizontal_index = self.ax, -1
|
||||
|
||||
while parent_menu do
|
||||
parent_offset_x = parent_offset_x - parent_menu.width - self.gap
|
||||
parent_offset_x = parent_offset_x - parent_menu.width - self.padding * 2 - self.gap
|
||||
draw_menu(parent_menu, parent_offset_x, parent_horizontal_index)
|
||||
parent_horizontal_index = parent_horizontal_index - 1
|
||||
parent_menu = parent_menu.parent_menu
|
||||
@@ -1830,4 +1822,4 @@ function Menu:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Menu
|
||||
return Menu
|
||||
@@ -14,7 +14,7 @@ function PauseIndicator:init()
|
||||
end
|
||||
|
||||
function PauseIndicator:init_options()
|
||||
self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
|
||||
self.base_icon_opacity = config.opacity.pause_indicator or (options.pause_indicator == 'flash' and 1) or 0.8
|
||||
self.type = options.pause_indicator
|
||||
self:on_prop_pause()
|
||||
end
|
||||
@@ -80,4 +80,4 @@ function PauseIndicator:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return PauseIndicator
|
||||
return PauseIndicator
|
||||
@@ -192,4 +192,4 @@ function Speed:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Speed
|
||||
return Speed
|
||||
@@ -18,12 +18,20 @@ function Timeline:init()
|
||||
self.progress_line_width = 0
|
||||
self.is_hovered = false
|
||||
self.has_thumbnail = false
|
||||
self.heatmap = nil
|
||||
|
||||
self:decide_progress_size()
|
||||
self:update_dimensions()
|
||||
|
||||
-- Release any dragging when file gets unloaded
|
||||
self:register_mp_event('end-file', function() self.pressed = false end)
|
||||
-- Load Youtube heatmap data if available
|
||||
self:register_mp_event('file-loaded', function()
|
||||
self.heatmap = load_youtube_heatmap()
|
||||
end)
|
||||
-- Release any dragging and clear heatmap when file gets unloaded
|
||||
self:register_mp_event('end-file', function()
|
||||
self.pressed = false
|
||||
self.heatmap = nil
|
||||
end)
|
||||
end
|
||||
|
||||
function Timeline:get_visibility()
|
||||
@@ -181,7 +189,7 @@ function Timeline:render()
|
||||
return
|
||||
end
|
||||
|
||||
if self.proximity_raw == 0 then
|
||||
if self.proximity_raw <= 0 then
|
||||
self.is_hovered = true
|
||||
end
|
||||
if visibility > 0 then
|
||||
@@ -257,7 +265,32 @@ function Timeline:render()
|
||||
ass:draw_stop()
|
||||
|
||||
-- Progress
|
||||
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
|
||||
local function draw_progress()
|
||||
ass:rect(fax, fay, fbx, fby, {opacity = config.opacity.position})
|
||||
end
|
||||
|
||||
-- Youtube heatmap
|
||||
local function draw_heatmap()
|
||||
if options.timeline_heatmap ~= 'no' and self.heatmap and config.opacity.heatmap > 0 and visibility > 0 then
|
||||
local is_above = options.timeline_heatmap == 'above'
|
||||
local height = math.min(40, size / self.size * 40)
|
||||
local ax, ay = bax, is_above and (bay - height) or (bay + self.top_border)
|
||||
local bx, by = bbx, is_above and bay or bby
|
||||
local opts = {color = config.color.heatmap, opacity = config.opacity.heatmap * visibility}
|
||||
local clip_ay = is_above and (ay - 10) or ay
|
||||
opts.clip = string.format('\\clip(%d,%d,%d,%d)', ax, clip_ay, bx, by)
|
||||
ass:smooth_curve(ax, ay, bx, by, self.heatmap, opts)
|
||||
end
|
||||
end
|
||||
|
||||
-- Change draw order based on 'timeline_style' to keep the heatmap visible
|
||||
if is_line then
|
||||
draw_heatmap()
|
||||
draw_progress()
|
||||
else
|
||||
draw_progress()
|
||||
draw_heatmap()
|
||||
end
|
||||
|
||||
-- Uncached ranges
|
||||
if state.uncached_ranges then
|
||||
@@ -380,7 +413,7 @@ function Timeline:render()
|
||||
|
||||
-- Time values
|
||||
if text_opacity > 0 then
|
||||
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2 * state.scale}
|
||||
local time_opts = {size = self.font_size, opacity = text_opacity, border = options.text_border * state.scale}
|
||||
-- Upcoming cache time
|
||||
local cache_duration = state.cache_duration and state.cache_duration / state.speed or nil
|
||||
if cache_duration and options.buffered_time_threshold > 0
|
||||
@@ -412,7 +445,7 @@ function Timeline:render()
|
||||
|
||||
-- Hovered time and chapter
|
||||
local rendered_thumbnail = false
|
||||
if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and not Elements:v('speed', 'dragging') then
|
||||
if (self.proximity_raw <= 0 or self.pressed or hovered_chapter) and not Elements:v('speed', 'dragging') then
|
||||
local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x
|
||||
local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x)
|
||||
|
||||
@@ -486,4 +519,4 @@ function Timeline:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Timeline
|
||||
return Timeline
|
||||
@@ -21,11 +21,17 @@ function TopBar:init()
|
||||
self.current_chapter = nil
|
||||
|
||||
local function maximized_command()
|
||||
mp.command(state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes')
|
||||
if state.platform == 'windows' then
|
||||
mp.command(state.border
|
||||
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
|
||||
or 'set window-maximized no;cycle fullscreen')
|
||||
else
|
||||
mp.command(state.fullormaxed and 'set fullscreen no;set window-maximized no' or 'set window-maximized yes')
|
||||
end
|
||||
end
|
||||
|
||||
local close = {icon = 'close', hover_bg = '2311e8', hover_fg = 'ffffff', command = function() mp.command('quit') end}
|
||||
local max = {icon = 'crop_square', command = maximized_command, is_max = true}
|
||||
local max = {icon = 'crop_square', command = maximized_command}
|
||||
local min = {icon = 'minimize', command = function() mp.command('cycle window-minimized') end}
|
||||
self.buttons = options.top_bar_controls == 'left' and {close, max, min} or {min, max, close}
|
||||
|
||||
@@ -237,13 +243,8 @@ function TopBar:render()
|
||||
end
|
||||
|
||||
for _, button in ipairs(self.buttons) do
|
||||
if button.is_max then
|
||||
button.icon = state.fullscreen and 'close_fullscreen' or
|
||||
(state.maximized and 'filter_none' or 'crop_square')
|
||||
end
|
||||
|
||||
local rect = {ax = button_ax, ay = ay, bx = button_ax + self.size, by = by}
|
||||
local is_hover = get_point_to_rectangle_proximity(cursor, rect) == 0
|
||||
local is_hover = get_point_to_rectangle_proximity(cursor, rect) <= 0
|
||||
local opacity = is_hover and 1 or config.opacity.controls
|
||||
local button_fg = is_hover and (button.hover_fg or bg) or fg
|
||||
local button_bg = is_hover and (button.hover_bg or fg) or bg
|
||||
@@ -290,7 +291,7 @@ function TopBar:render()
|
||||
bx = ax + rect_width,
|
||||
by = by - margin,
|
||||
}
|
||||
local opacity = get_point_to_rectangle_proximity(cursor, rect) == 0
|
||||
local opacity = get_point_to_rectangle_proximity(cursor, rect) <= 0
|
||||
and 1 or config.opacity.playlist_position
|
||||
if opacity > 0 then
|
||||
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
||||
@@ -427,4 +428,4 @@ function TopBar:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return TopBar
|
||||
return TopBar
|
||||
@@ -0,0 +1,319 @@
|
||||
local Element = require('elements/Element')
|
||||
local dots = {'.', '..', '...'}
|
||||
|
||||
local function cleanup_output(output)
|
||||
return tostring(output):gsub('%c*\n%c*', '\n'):match('^[%s%c]*(.-)[%s%c]*$')
|
||||
end
|
||||
|
||||
---@class Updater : Element
|
||||
local Updater = class(Element)
|
||||
|
||||
function Updater:new() return Class.new(self) --[[@as Updater]] end
|
||||
function Updater:init()
|
||||
Element.init(self, 'updater', {render_order = 1000})
|
||||
self.output = nil
|
||||
self.title = ''
|
||||
self.state = 'circle' -- Also used as an icon name. 'pending' maps to 'spinner'.
|
||||
self.update_available = false
|
||||
|
||||
-- Buttons
|
||||
self.check_button = {method = 'check', title = t('Check for updates')}
|
||||
self.update_button = {method = 'update', title = t('Update uosc'), color = config.color.success}
|
||||
self.changelog_button = {method = 'open_changelog', title = t('Open changelog')}
|
||||
self.close_button = {method = 'destroy', title = t('Close') .. ' (Esc)', color = config.color.error}
|
||||
self.quit_button = {method = 'quit', title = t('Quit')}
|
||||
self.buttons = {self.check_button, self.close_button}
|
||||
self.selected_button_index = 1
|
||||
|
||||
-- Key bindings
|
||||
self:add_key_binding('right', 'select_next_button')
|
||||
self:add_key_binding('tab', 'select_next_button')
|
||||
self:add_key_binding('left', 'select_prev_button')
|
||||
self:add_key_binding('shift+tab', 'select_prev_button')
|
||||
self:add_key_binding('enter', 'activate_selected_button')
|
||||
self:add_key_binding('kp_enter', 'activate_selected_button')
|
||||
self:add_key_binding('esc', 'destroy')
|
||||
|
||||
Elements:maybe('curtain', 'register', self.id)
|
||||
self:check()
|
||||
end
|
||||
|
||||
function Updater:destroy()
|
||||
Elements:maybe('curtain', 'unregister', self.id)
|
||||
Element.destroy(self)
|
||||
end
|
||||
|
||||
function Updater:quit()
|
||||
mp.command('quit')
|
||||
end
|
||||
|
||||
function Updater:select_prev_button()
|
||||
self.selected_button_index = self.selected_button_index - 1
|
||||
if self.selected_button_index < 1 then self.selected_button_index = #self.buttons end
|
||||
request_render()
|
||||
end
|
||||
|
||||
function Updater:select_next_button()
|
||||
self.selected_button_index = self.selected_button_index + 1
|
||||
if self.selected_button_index > #self.buttons then self.selected_button_index = 1 end
|
||||
request_render()
|
||||
end
|
||||
|
||||
function Updater:activate_selected_button()
|
||||
local button = self.buttons[self.selected_button_index]
|
||||
if button then self[button.method](self) end
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
function Updater:append_output(msg)
|
||||
self.output = (self.output or '') .. ass_escape('\n' .. cleanup_output(msg))
|
||||
request_render()
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
function Updater:display_error(msg)
|
||||
self.state = 'error'
|
||||
self.title = t('An error has occurred.') .. ' ' .. t('See console for details.')
|
||||
self:append_output(msg)
|
||||
print(msg)
|
||||
end
|
||||
|
||||
function Updater:open_changelog()
|
||||
if self.state == 'pending' then return end
|
||||
|
||||
local url = 'https://github.com/tomasklaen/uosc/releases'
|
||||
|
||||
self:append_output('Opening URL: ' .. url)
|
||||
|
||||
call_ziggy_async({'open', url}, function(error)
|
||||
if error then
|
||||
self:display_error(error)
|
||||
return
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Updater:check()
|
||||
if self.state == 'pending' then return end
|
||||
self.state = 'pending'
|
||||
self.title = t('Checking for updates') .. '...'
|
||||
|
||||
local url = 'https://api.github.com/repos/tomasklaen/uosc/releases/latest'
|
||||
local headers = utils.format_json({
|
||||
Accept = 'application/vnd.github+json',
|
||||
})
|
||||
local args = {'http-get', '--headers', headers, url}
|
||||
|
||||
self:append_output('Fetching: ' .. url)
|
||||
|
||||
call_ziggy_async(args, function(error, response)
|
||||
if error then
|
||||
self:display_error(error)
|
||||
return
|
||||
end
|
||||
|
||||
release = utils.parse_json(type(response.body) == 'string' and response.body or '')
|
||||
if response.status == 200 and type(release) == 'table' and type(release.tag_name) == 'string' then
|
||||
self.update_available = config.version ~= release.tag_name
|
||||
self:append_output('Response: 200 OK')
|
||||
self:append_output('Current version: ' .. config.version)
|
||||
self:append_output('Latest version: ' .. release.tag_name)
|
||||
if self.update_available then
|
||||
self.state = 'upgrade'
|
||||
self.title = t('Update available')
|
||||
self.buttons = {self.update_button, self.changelog_button, self.close_button}
|
||||
self.selected_button_index = 1
|
||||
else
|
||||
self.state = 'done'
|
||||
self.title = t('Up to date')
|
||||
end
|
||||
else
|
||||
self:display_error('Response couldn\'t be parsed, is invalid, or not-OK status code.\nStatus: ' ..
|
||||
response.status .. '\nBody: ' .. response.body)
|
||||
end
|
||||
|
||||
request_render()
|
||||
end)
|
||||
end
|
||||
|
||||
function Updater:update()
|
||||
if self.state == 'pending' then return end
|
||||
self.state = 'pending'
|
||||
self.title = t('Updating uosc')
|
||||
self.output = nil
|
||||
request_render()
|
||||
|
||||
local config_dir = mp.command_native({'expand-path', '~~/'})
|
||||
|
||||
local function handle_result(success, result, error)
|
||||
if success and result and result.status == 0 then
|
||||
self.state = 'done'
|
||||
self.title = t('uosc has been installed. Restart mpv for it to take effect.')
|
||||
self.buttons = {self.quit_button, self.close_button}
|
||||
self.selected_button_index = 1
|
||||
else
|
||||
self.state = 'error'
|
||||
self.title = t('An error has occurred.') .. ' ' .. t('See above for clues.')
|
||||
end
|
||||
|
||||
local output = (result.stdout or '') .. '\n' .. (error or result.stderr or '')
|
||||
if state.platform == 'darwin' then
|
||||
output =
|
||||
'Self-updater is known not to work on MacOS.\nIf you know about a solution, please make an issue and share it with us!.\n' ..
|
||||
output
|
||||
end
|
||||
self:append_output(output)
|
||||
end
|
||||
|
||||
local function update(args)
|
||||
local env = utils.get_env_list()
|
||||
env[#env + 1] = 'MPV_CONFIG_DIR=' .. config_dir
|
||||
|
||||
mp.command_native_async({
|
||||
name = 'subprocess',
|
||||
capture_stderr = true,
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
args = args,
|
||||
env = env,
|
||||
}, handle_result)
|
||||
end
|
||||
|
||||
if state.platform == 'windows' then
|
||||
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/windows.ps1'
|
||||
update({'powershell', '-NoProfile', '-Command', 'irm ' .. url .. ' | iex'})
|
||||
else
|
||||
-- Detect missing dependencies. We can't just let the process run and
|
||||
-- report an error, as on snap packages there's no error. Everything
|
||||
-- either exits with 0, or no helpful output/error message.
|
||||
local missing = {}
|
||||
|
||||
for _, name in ipairs({'curl', 'unzip'}) do
|
||||
local result = mp.command_native({
|
||||
name = 'subprocess',
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
args = {'which', name},
|
||||
})
|
||||
local path = cleanup_output(result and result.stdout or '')
|
||||
if path == '' then
|
||||
missing[#missing + 1] = name
|
||||
end
|
||||
end
|
||||
|
||||
if #missing > 0 then
|
||||
local stderr = 'Missing dependencies: ' .. table.concat(missing, ', ')
|
||||
if config_dir:match('/snap/') then
|
||||
stderr = stderr ..
|
||||
'\nThis is a known error for mpv snap packages.\nYou can still update uosc by entering the Linux install command from uosc\'s readme into your terminal, it just can\'t be done this way.\nIf you know about a solution, please make an issue and share it with us!'
|
||||
end
|
||||
handle_result(false, {stderr = stderr})
|
||||
else
|
||||
local url = 'https://raw.githubusercontent.com/tomasklaen/uosc/HEAD/installers/unix.sh'
|
||||
update({'/bin/bash', '-c', 'source <(curl -fsSL ' .. url .. ')'})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Updater:render()
|
||||
local ass = assdraw.ass_new()
|
||||
|
||||
local text_size = math.min(20 * state.scale, display.height / 20)
|
||||
local icon_size = text_size * 2
|
||||
local center_x = round(display.width / 2)
|
||||
|
||||
local color = fg
|
||||
if self.state == 'done' or self.update_available then
|
||||
color = config.color.success
|
||||
elseif self.state == 'error' then
|
||||
color = config.color.error
|
||||
end
|
||||
|
||||
-- Divider
|
||||
local divider_width = round(math.min(500 * state.scale, display.width * 0.8))
|
||||
local divider_half, divider_border_half, divider_y = divider_width / 2, round(1 * state.scale), display.height * 0.65
|
||||
local divider_ay, divider_by = round(divider_y - divider_border_half), round(divider_y + divider_border_half)
|
||||
ass:rect(center_x - divider_half, divider_ay, center_x - icon_size, divider_by, {
|
||||
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
||||
})
|
||||
ass:rect(center_x + icon_size, divider_ay, center_x + divider_half, divider_by, {
|
||||
color = color, border = options.text_border * state.scale, border_color = bg, opacity = 0.5,
|
||||
})
|
||||
if self.state == 'pending' then
|
||||
ass:spinner(center_x, divider_y, icon_size, {
|
||||
color = fg, border = options.text_border * state.scale, border_color = bg,
|
||||
})
|
||||
else
|
||||
ass:icon(center_x, divider_y, icon_size * 0.8, self.state, {
|
||||
color = color, border = options.text_border * state.scale, border_color = bg,
|
||||
})
|
||||
end
|
||||
|
||||
-- Output
|
||||
local output = self.output or dots[math.ceil((mp.get_time() % 1) * #dots)]
|
||||
ass:txt(center_x, divider_y - icon_size, 2, output, {
|
||||
size = text_size, color = fg, border = options.text_border * state.scale, border_color = bg,
|
||||
})
|
||||
|
||||
-- Title
|
||||
ass:txt(center_x, divider_y + icon_size, 5, self.title, {
|
||||
size = text_size, bold = true, color = color, border = options.text_border * state.scale, border_color = bg,
|
||||
})
|
||||
|
||||
-- Buttons
|
||||
local outline = round(1 * state.scale)
|
||||
local spacing = outline * 9
|
||||
local padding = round(text_size * 0.5)
|
||||
|
||||
local text_opts = {size = text_size, bold = true}
|
||||
|
||||
-- Calculate button text widths
|
||||
local total_width = (#self.buttons - 1) * spacing
|
||||
for _, button in ipairs(self.buttons) do
|
||||
button.width = text_width(button.title, text_opts) + padding * 2
|
||||
total_width = total_width + button.width
|
||||
end
|
||||
|
||||
-- Render buttons
|
||||
local ay = round(divider_y + icon_size * 1.8)
|
||||
local ax = round(display.width / 2 - total_width / 2)
|
||||
local height = text_size + padding * 2
|
||||
for index, button in ipairs(self.buttons) do
|
||||
local rect = {
|
||||
ax = ax,
|
||||
ay = ay,
|
||||
bx = ax + button.width,
|
||||
by = ay + height,
|
||||
}
|
||||
ax = rect.bx + spacing
|
||||
local is_hovered = get_point_to_rectangle_proximity(cursor, rect) <= 0
|
||||
|
||||
-- Background
|
||||
ass:rect(rect.ax, rect.ay, rect.bx, rect.by, {
|
||||
color = button.color or fg,
|
||||
radius = state.radius,
|
||||
opacity = is_hovered and 1 or 0.8,
|
||||
})
|
||||
-- Selected outline
|
||||
if index == self.selected_button_index then
|
||||
ass:rect(rect.ax - outline * 4, rect.ay - outline * 4, rect.bx + outline * 4, rect.by + outline * 4, {
|
||||
border = outline,
|
||||
border_color = button.color or fg,
|
||||
radius = state.radius + outline * 4,
|
||||
opacity = {primary = 0, border = 0.5},
|
||||
})
|
||||
end
|
||||
-- Text
|
||||
local x, y = rect.ax + (rect.bx - rect.ax) / 2, rect.ay + (rect.by - rect.ay) / 2
|
||||
ass:txt(x, y, 5, button.title, {size = text_size, bold = true, color = fgt})
|
||||
|
||||
cursor:zone('primary_down', rect, self:create_action(button.method))
|
||||
|
||||
-- Select hovered button
|
||||
if is_hovered then self.selected_button_index = index end
|
||||
end
|
||||
|
||||
return ass
|
||||
end
|
||||
|
||||
return Updater
|
||||
@@ -242,6 +242,7 @@ end
|
||||
function Volume:on_display() self:update_dimensions() end
|
||||
function Volume:on_prop_border() self:update_dimensions() end
|
||||
function Volume:on_prop_title_bar() self:update_dimensions() end
|
||||
function Volume:on_prop_volume_max() self:update_dimensions() end
|
||||
function Volume:on_controls_reflow() self:update_dimensions() end
|
||||
function Volume:on_options() self:update_dimensions() end
|
||||
|
||||
@@ -280,4 +281,4 @@ function Volume:render()
|
||||
return ass
|
||||
end
|
||||
|
||||
return Volume
|
||||
return Volume
|
||||
@@ -26,10 +26,10 @@ function WindowBorder:render()
|
||||
local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' ..
|
||||
(display.width - self.size) .. ',' .. (display.height - self.size) .. ')'
|
||||
ass:rect(0, 0, display.width + 1, display.height + 1, {
|
||||
color = bg, clip = clip, opacity = config.opacity.border,
|
||||
color = config.color.window_border, clip = clip, opacity = config.opacity.border,
|
||||
})
|
||||
return ass
|
||||
end
|
||||
end
|
||||
|
||||
return WindowBorder
|
||||
return WindowBorder
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user