Compare commits
No commits in common. "72af217e8cd8142671e13f51525c42c4032181bc" and "36d411dafa7306bfa6847905570529f34b83caca" have entirely different histories.
72af217e8c
...
36d411dafa
51 changed files with 3742 additions and 7179 deletions
|
@ -301,7 +301,6 @@ ttf-comic-neue Comic Neue aspires to be the casual script choice for everyone in
|
||||||
ttf-heuristica A serif latin & cyrillic font, derived from the "Adobe Utopia" font by Apanov A
|
ttf-heuristica A serif latin & cyrillic font, derived from the "Adobe Utopia" font by Apanov A
|
||||||
ttf-iosevka-nerd Patched font Iosevka from nerd fonts library R
|
ttf-iosevka-nerd Patched font Iosevka from nerd fonts library R
|
||||||
ttf-signika Sans-serif typeface from Google by Anna Giedryś A
|
ttf-signika Sans-serif typeface from Google by Anna Giedryś A
|
||||||
ttf-victor-mono-nerd Patched font Victor Mono from nerd fonts library R
|
|
||||||
tuir Browse Reddit from your terminal A
|
tuir Browse Reddit from your terminal A
|
||||||
tut A TUI for Mastodon with vim inspired keys A
|
tut A TUI for Mastodon with vim inspired keys A
|
||||||
typescript-language-server Language Server Protocol (LSP) implementation for TypeScript using tsserver R
|
typescript-language-server Language Server Protocol (LSP) implementation for TypeScript using tsserver R
|
||||||
|
|
Can't render this file because it contains an unexpected character in line 301 and column 64.
|
|
@ -1 +0,0 @@
|
||||||
ACTION=="add", SUBSYSTEM=="usb", DRIVERS=="usb", ATTRS{idVendor}=="0000", ATTRS{idProduct}=="3825", ATTR{power/wakeup}="disabled"
|
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"layer": "top",
|
"layer": "top",
|
||||||
"modules-left": ["river/tags", "custom/events", "custom/vidl", "river/window"],
|
"modules-left": ["river/tags", "custom/events", "custom/vidl"],
|
||||||
"modules-center": ["clock", "custom/media"],
|
"modules-center": ["clock", "custom/media"],
|
||||||
"modules-right": ["river/mode", "custom/wireguard", "custom/archupdates", "pulseaudio", "backlight", "network", "cpu", "memory", "temperature", "battery", "tray"],
|
"modules-right": ["custom/wireguard", "custom/archupdates", "pulseaudio", "backlight", "network", "cpu", "memory", "temperature", "battery", "tray"],
|
||||||
"custom/archupdates": {
|
"custom/archupdates": {
|
||||||
"format": "{} {icon}",
|
"format": "{} {icon}",
|
||||||
"format-alt-click": "right",
|
"format-alt-click": "right",
|
||||||
"format-icons": {
|
"format-icons": {
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
"exec": "~/.config/waybar/modules/archupdates 5 json",
|
"exec": "~/.config/waybar/modules/archupdates 5 json",
|
||||||
|
@ -56,8 +56,7 @@
|
||||||
},
|
},
|
||||||
"exec": "~/.config/waybar/modules/khal.py 2>/dev/null",
|
"exec": "~/.config/waybar/modules/khal.py 2>/dev/null",
|
||||||
"exec-if": "command -v khal >/dev/null 2>&1",
|
"exec-if": "command -v khal >/dev/null 2>&1",
|
||||||
"return-type": "json",
|
"return-type": "json"
|
||||||
"on-click": "$TERMINAL start --class float ikhal"
|
|
||||||
},
|
},
|
||||||
"memory": {
|
"memory": {
|
||||||
"interval": 30,
|
"interval": 30,
|
||||||
|
@ -106,13 +105,14 @@
|
||||||
"on-click-right": "playerctl stop",
|
"on-click-right": "playerctl stop",
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
|
"interface": "wlp58s0",
|
||||||
"format": "{ifname}",
|
"format": "{ifname}",
|
||||||
"format-wifi": "{signalStrength}% ",
|
"format-wifi": "{signalStrength}% ",
|
||||||
"format-ethernet": "{ipaddr}/{cidr} ",
|
"format-ethernet": "{ipaddr}/{cidr} ",
|
||||||
"format-disconnected": "",
|
"format-disconnected": "睊",
|
||||||
"tooltip-format": "{ifname} via {gwaddr} ",
|
"tooltip-format": "{ifname} via {gwaddr} ",
|
||||||
"tooltip-format-wifi": "{essid}: {bandwidthDownBits}-{bandwidthUpBits} ({signalStrength}%) {ifname}",
|
"tooltip-format-wifi": "{essid}: {bandwidthDownBits}-{bandwidthUpBits} ({signalStrength}%) {ifname}",
|
||||||
"tooltip-format-ethernet": "{ifname} ",
|
"tooltip-format-ethernet": "{ifname} ",
|
||||||
"tooltip-format-disconnected": "Disconnected",
|
"tooltip-format-disconnected": "Disconnected",
|
||||||
"max-length": 50,
|
"max-length": 50,
|
||||||
"on-click": "$TERMINAL start --class float nmtui",
|
"on-click": "$TERMINAL start --class float nmtui",
|
||||||
|
@ -120,12 +120,12 @@
|
||||||
},
|
},
|
||||||
"pulseaudio": {
|
"pulseaudio": {
|
||||||
"format": "{volume}% {icon}",
|
"format": "{volume}% {icon}",
|
||||||
"format-bluetooth": "{volume}% {icon} ",
|
"format-bluetooth": "{volume}% {icon}",
|
||||||
"format-muted": "",
|
"format-muted": "",
|
||||||
"format-icons": {
|
"format-icons": {
|
||||||
"headphone": "",
|
"headphone": "",
|
||||||
"hands-free": "",
|
"hands-free": "",
|
||||||
"headset": "",
|
"headset": "",
|
||||||
"phone": "",
|
"phone": "",
|
||||||
"portable": "",
|
"portable": "",
|
||||||
"car": "",
|
"car": "",
|
||||||
|
@ -140,13 +140,6 @@
|
||||||
"num-tags": 10,
|
"num-tags": 10,
|
||||||
"tag-labels": [ "", "", "", "", "", "", "", "", "", "" ]
|
"tag-labels": [ "", "", "", "", "", "", "", "", "", "" ]
|
||||||
},
|
},
|
||||||
"river/mode": {
|
|
||||||
"format": "{} ",
|
|
||||||
},
|
|
||||||
"river/window": {
|
|
||||||
"format": " {}",
|
|
||||||
"max-length": 70
|
|
||||||
},
|
|
||||||
"temperature": {
|
"temperature": {
|
||||||
// "thermal-zone": 2,
|
// "thermal-zone": 2,
|
||||||
"hwmon-path": "/sys/class/hwmon/hwmon5/temp1_input",
|
"hwmon-path": "/sys/class/hwmon/hwmon5/temp1_input",
|
||||||
|
@ -166,7 +159,7 @@
|
||||||
},
|
},
|
||||||
"custom/wireguard": {
|
"custom/wireguard": {
|
||||||
"format-icons": {
|
"format-icons": {
|
||||||
"default": ""
|
"default": "嬨"
|
||||||
},
|
},
|
||||||
"exec": "~/.config/waybar/modules/wireguard json",
|
"exec": "~/.config/waybar/modules/wireguard json",
|
||||||
"exec-if": "command -v nmcli >/dev/null 2>&1",
|
"exec-if": "command -v nmcli >/dev/null 2>&1",
|
||||||
|
@ -178,7 +171,7 @@
|
||||||
"format": "{} {icon}",
|
"format": "{} {icon}",
|
||||||
"format-alt-click": "right",
|
"format-alt-click": "right",
|
||||||
"format-icons": {
|
"format-icons": {
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"exec": "wc -l ~/.local/share/vidl/vidl_queue | cut -d' ' -f1",
|
"exec": "wc -l ~/.local/share/vidl/vidl_queue | cut -d' ' -f1",
|
||||||
"exec-if": "[ -f ~/.local/share/vidl/vidl_queue ]",
|
"exec-if": "[ -f ~/.local/share/vidl/vidl_queue ]",
|
||||||
|
|
|
@ -41,7 +41,7 @@ connected=()
|
||||||
available=()
|
available=()
|
||||||
|
|
||||||
function print_as_json() {
|
function print_as_json() {
|
||||||
text="" # only prints a single icon when connected
|
text="嬨" # only prints a single icon when connected
|
||||||
# text="${1}" # use this line to show all output in text
|
# text="${1}" # use this line to show all output in text
|
||||||
alt="${1}"
|
alt="${1}"
|
||||||
tooltip="${1}"
|
tooltip="${1}"
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
font-family: Cantarell, Signika, Iosevka Nerd Font, Iosevka, monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window#waybar {
|
window#waybar {
|
||||||
|
@ -58,7 +57,6 @@ window#waybar.hidden {
|
||||||
#mode,
|
#mode,
|
||||||
#idle_inhibitor,
|
#idle_inhibitor,
|
||||||
#mpd,
|
#mpd,
|
||||||
#window,
|
|
||||||
#custom-archupdates,
|
#custom-archupdates,
|
||||||
#custom-wireguard,
|
#custom-wireguard,
|
||||||
#custom-events,
|
#custom-events,
|
||||||
|
@ -84,14 +82,6 @@ window#waybar.hidden {
|
||||||
background-color: @base02;
|
background-color: @base02;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mark active output through highlighted window background */
|
|
||||||
#window {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
#window.focused {
|
|
||||||
background-color: @base01;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.warning {
|
#battery.warning {
|
||||||
background-color: @base09;
|
background-color: @base09;
|
||||||
color: @base00;
|
color: @base00;
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -37,8 +37,7 @@ I script-message playlistmanager show filename
|
||||||
R cycle-values video-aspect "16:9" "4:3" "3.25:1" "no" "-1" # cycle aspect ratios
|
R cycle-values video-aspect "16:9" "4:3" "3.25:1" "no" "-1" # cycle aspect ratios
|
||||||
|
|
||||||
# uosc definitions and menu
|
# uosc definitions and menu
|
||||||
o script-message-to uosc toggle-elements timeline
|
o script-binding uosc/peek-timeline
|
||||||
O script-binding uosc/toggle-ui
|
|
||||||
m script-binding uosc/menu
|
m script-binding uosc/menu
|
||||||
|
|
||||||
# script-binding uosc/open-file #! Open file
|
# script-binding uosc/open-file #! Open file
|
||||||
|
@ -49,4 +48,3 @@ m script-binding uosc/menu
|
||||||
# script-binding uosc/playlist #! Utils > Playlist
|
# script-binding uosc/playlist #! Utils > Playlist
|
||||||
# script-binding uosc/chapters #! Utils > Chapters
|
# script-binding uosc/chapters #! Utils > Chapters
|
||||||
# script-binding uosc/open-config-directory #! Utils > Open config directory
|
# script-binding uosc/open-config-directory #! Utils > Open config directory
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
-- If the laptop is on battery, the profile 'lq' will be loaded; otherwise 'hq' is used
|
-- If the laptop is on battery, the profile 'lq' will be loaded; otherwise 'hq' is used
|
||||||
local mp = require 'mp'
|
--
|
||||||
|
|
||||||
local SHOULD_ADJUST = false
|
|
||||||
|
|
||||||
local lqprofile = "lowquality"
|
local lqprofile = "lowquality"
|
||||||
local hqprofile = "highquality"
|
local hqprofile = "highquality"
|
||||||
|
|
||||||
|
@ -15,19 +12,14 @@ local function powerstate()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function adjust()
|
local function adjust()
|
||||||
if not SHOULD_ADJUST then return end
|
|
||||||
|
|
||||||
local state = powerstate()
|
local state = powerstate()
|
||||||
-- this actually overrides automatically applied profiles
|
-- this actually overrides automatically applied profiles
|
||||||
-- like 'protocol.http'
|
-- like 'protocol.http'
|
||||||
if state == 0 then
|
if state == 0 then
|
||||||
|
mp.msg.info("Running on battery, setting low-quality options.")
|
||||||
mp.set_property("profile", lqprofile)
|
mp.set_property("profile", lqprofile)
|
||||||
mp.msg.info("[quality] running battery, setting low-quality options.")
|
|
||||||
mp.osd_message("[quality] LQ")
|
|
||||||
else
|
else
|
||||||
mp.set_property("profile", hqprofile)
|
mp.msg.info("Not running on battery, setting high-quality options.")
|
||||||
mp.msg.info("[quality] running ac, setting high-quality options.")
|
|
||||||
mp.osd_message("[quality] HQ")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
mp.add_hook("on_load", 1, adjust)
|
mp.add_hook("on_load", 1, adjust)
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
local mp = require 'mp'
|
|
||||||
require 'mp.msg'
|
|
||||||
|
|
||||||
-- Copy the current time of the video to clipboard.
|
|
||||||
|
|
||||||
WINDOWS = 2
|
|
||||||
UNIX = 3
|
|
||||||
KEY_BIND = "y"
|
|
||||||
|
|
||||||
local function platform_type()
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
local workdir = utils.to_string(mp.get_property_native("working-directory"))
|
|
||||||
if string.find(workdir, "\\") then
|
|
||||||
return WINDOWS
|
|
||||||
else
|
|
||||||
return UNIX
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function command_exists(cmd)
|
|
||||||
local pipe = io.popen("type " .. cmd .. " > /dev/null 2> /dev/null; printf \"$?\"", "r")
|
|
||||||
if not pipe then return end
|
|
||||||
local exists = pipe:read() == "0"
|
|
||||||
pipe:close()
|
|
||||||
return exists
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_clipboard_cmd()
|
|
||||||
if command_exists("xclip") then
|
|
||||||
return "xclip -silent -in -selection clipboard"
|
|
||||||
elseif command_exists("wl-copy") then
|
|
||||||
return "wl-copy"
|
|
||||||
elseif command_exists("pbcopy") then
|
|
||||||
return "pbcopy"
|
|
||||||
else
|
|
||||||
mp.msg.error("No supported clipboard command found")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function divmod(a, b)
|
|
||||||
return a / b, a % b
|
|
||||||
end
|
|
||||||
|
|
||||||
local function set_clipboard(text)
|
|
||||||
if platform == WINDOWS then
|
|
||||||
mp.commandv("run", "powershell", "set-clipboard", text)
|
|
||||||
return true
|
|
||||||
elseif (platform == UNIX and clipboard_cmd) then
|
|
||||||
local pipe = io.popen(clipboard_cmd, "w")
|
|
||||||
if not pipe then return end
|
|
||||||
pipe:write(text)
|
|
||||||
pipe:close()
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
mp.msg.error("Set_clipboard error")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function copyTime()
|
|
||||||
local time_pos = mp.get_property_number("time-pos")
|
|
||||||
local minutes, remainder = divmod(time_pos, 60)
|
|
||||||
local hours, minutes = divmod(minutes, 60)
|
|
||||||
local seconds = math.floor(remainder)
|
|
||||||
local milliseconds = math.floor((remainder - seconds) * 1000)
|
|
||||||
local time = string.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds)
|
|
||||||
if set_clipboard(time) then
|
|
||||||
mp.osd_message(string.format("[copytime] %s", time))
|
|
||||||
else
|
|
||||||
mp.osd_message("[copytime] failed")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
platform = platform_type()
|
|
||||||
if platform == UNIX then
|
|
||||||
clipboard_cmd = get_clipboard_cmd()
|
|
||||||
end
|
|
||||||
mp.add_key_binding(KEY_BIND, "copyTime", copyTime)
|
|
|
@ -7,24 +7,23 @@
|
||||||
-- e.g.
|
-- e.g.
|
||||||
-- `mpv gallery-dl://https://imgur.com/....`
|
-- `mpv gallery-dl://https://imgur.com/....`
|
||||||
|
|
||||||
local mp = require 'mp'
|
|
||||||
local utils = require 'mp.utils'
|
local utils = require 'mp.utils'
|
||||||
local msg = require 'mp.msg'
|
local msg = require 'mp.msg'
|
||||||
|
|
||||||
local function exec(args)
|
local function exec(args)
|
||||||
local ret = utils.subprocess({ args = args })
|
local ret = utils.subprocess({args = args})
|
||||||
return ret.status, ret.stdout, ret
|
return ret.status, ret.stdout, ret
|
||||||
end
|
end
|
||||||
|
|
||||||
mp.add_hook("on_load", 15, function()
|
mp.add_hook("on_load", 15, function()
|
||||||
local fn = mp.get_property("stream-open-filename", "")
|
local url = mp.get_property("stream-open-filename", "")
|
||||||
if (fn:find("gdl://") ~= 1) then
|
if (url:find("gdl://") ~= 1) then
|
||||||
msg.debug("not a gdl:// url: " .. fn)
|
msg.debug("not a gdl:// url: " .. url)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local url = string.gsub(url, "gdl://", "")
|
local url = string.gsub(url,"gdl://","")
|
||||||
|
|
||||||
local es, urls, result = exec({ "gallery-dl", "-g", url })
|
local es, urls, result = exec({"gallery-dl", "-g", url})
|
||||||
if (es < 0) or (urls == nil) or (urls == "") then
|
if (es < 0) or (urls == nil) or (urls == "") then
|
||||||
msg.error("failed to get album list.")
|
msg.error("failed to get album list.")
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
--
|
--
|
||||||
-- original from https://codeberg.org/jouni/mpv_sponsorblock_minimal
|
-- original from https://codeberg.org/jouni/mpv_sponsorblock_minimal
|
||||||
-- adapted for local playback skipping and some refactoring by me
|
-- adapted for local playback skipping and some refactoring by me
|
||||||
local mp = require 'mp'
|
|
||||||
|
|
||||||
local options = {
|
local options = {
|
||||||
API = "https://sponsor.ajay.app/api/skipSegments",
|
API = "https://sponsor.ajay.app/api/skipSegments",
|
||||||
|
|
||||||
|
@ -33,9 +31,10 @@ local function getranges()
|
||||||
Ranges[k] = v
|
Ranges[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local function skip_ads(_, pos)
|
local function skip_ads(name, pos)
|
||||||
if pos ~= nil then
|
if pos ~= nil then
|
||||||
for k, v in pairs(Ranges) do
|
for k, v in pairs(Ranges) do
|
||||||
if tonumber(k) <= pos and tonumber(v) > pos then
|
if tonumber(k) <= pos and tonumber(v) > pos then
|
||||||
|
@ -52,6 +51,7 @@ local function skip_ads(_, pos)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local function file_loaded()
|
local function file_loaded()
|
||||||
|
|
|
@ -1,975 +0,0 @@
|
||||||
-- thumbfast.lua
|
|
||||||
--
|
|
||||||
-- High-performance on-the-fly thumbnailer
|
|
||||||
--
|
|
||||||
-- Built for easy integration in third-party UIs.
|
|
||||||
|
|
||||||
local options = {
|
|
||||||
-- Socket path (leave empty for auto)
|
|
||||||
socket = "",
|
|
||||||
|
|
||||||
-- Thumbnail path (leave empty for auto)
|
|
||||||
thumbnail = "",
|
|
||||||
|
|
||||||
-- Maximum thumbnail size in pixels (scaled down to fit)
|
|
||||||
-- Values are scaled when hidpi is enabled
|
|
||||||
max_height = 200,
|
|
||||||
max_width = 200,
|
|
||||||
|
|
||||||
-- Apply tone-mapping, no to disable
|
|
||||||
tone_mapping = "auto",
|
|
||||||
|
|
||||||
-- Overlay id
|
|
||||||
overlay_id = 42,
|
|
||||||
|
|
||||||
-- Spawn thumbnailer on file load for faster initial thumbnails
|
|
||||||
spawn_first = false,
|
|
||||||
|
|
||||||
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
|
||||||
quit_after_inactivity = 0,
|
|
||||||
|
|
||||||
-- Enable on network playback
|
|
||||||
network = false,
|
|
||||||
|
|
||||||
-- Enable on audio playback
|
|
||||||
audio = false,
|
|
||||||
|
|
||||||
-- Enable hardware decoding
|
|
||||||
hwdec = false,
|
|
||||||
|
|
||||||
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
|
||||||
direct_io = false,
|
|
||||||
|
|
||||||
-- Custom path to the mpv executable
|
|
||||||
mpv_path = "mpv"
|
|
||||||
}
|
|
||||||
|
|
||||||
local mp = require "mp"
|
|
||||||
mp.utils = require "mp.utils"
|
|
||||||
mp.options = require "mp.options"
|
|
||||||
mp.options.read_options(options, "thumbfast")
|
|
||||||
|
|
||||||
local properties = {}
|
|
||||||
local pre_0_30_0 = mp.command_native_async == nil
|
|
||||||
local pre_0_33_0 = true
|
|
||||||
|
|
||||||
function subprocess(args, async, callback)
|
|
||||||
callback = callback or function()
|
|
||||||
end
|
|
||||||
|
|
||||||
if not pre_0_30_0 then
|
|
||||||
if async then
|
|
||||||
return mp.command_native_async({ name = "subprocess", playback_only = true, args = args }, callback)
|
|
||||||
else
|
|
||||||
return mp.command_native({ name = "subprocess", playback_only = false, capture_stdout = true, args = args })
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if async then
|
|
||||||
return mp.utils.subprocess_detached({ args = args }, callback)
|
|
||||||
else
|
|
||||||
return mp.utils.subprocess({ args = args })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local winapi = {}
|
|
||||||
if options.direct_io then
|
|
||||||
local ffi_loaded, ffi = pcall(require, "ffi")
|
|
||||||
if ffi_loaded then
|
|
||||||
winapi = {
|
|
||||||
ffi = ffi,
|
|
||||||
C = ffi.C,
|
|
||||||
bit = require("bit"),
|
|
||||||
socket_wc = "",
|
|
||||||
|
|
||||||
-- WinAPI constants
|
|
||||||
CP_UTF8 = 65001,
|
|
||||||
GENERIC_WRITE = 0x40000000,
|
|
||||||
OPEN_EXISTING = 3,
|
|
||||||
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
|
||||||
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
|
||||||
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
|
||||||
|
|
||||||
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
|
||||||
|
|
||||||
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
|
||||||
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
|
||||||
}
|
|
||||||
-- cache flags used in run() to avoid bor() call
|
|
||||||
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
|
||||||
|
|
||||||
ffi.cdef [[
|
|
||||||
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
|
||||||
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
|
||||||
bool __stdcall CloseHandle(void *hObject);
|
|
||||||
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
|
||||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
|
||||||
]]
|
|
||||||
|
|
||||||
winapi.MultiByteToWideChar = function(MultiByteStr)
|
|
||||||
if MultiByteStr then
|
|
||||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -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, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
|
||||||
return utf16_str
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ""
|
|
||||||
end
|
|
||||||
else
|
|
||||||
options.direct_io = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local file = nil
|
|
||||||
local file_bytes = 0
|
|
||||||
local spawned = false
|
|
||||||
local disabled = false
|
|
||||||
local force_disabled = false
|
|
||||||
local spawn_waiting = false
|
|
||||||
local spawn_working = false
|
|
||||||
local script_written = false
|
|
||||||
|
|
||||||
local dirty = false
|
|
||||||
|
|
||||||
local x = nil
|
|
||||||
local y = nil
|
|
||||||
local last_x = x
|
|
||||||
local last_y = y
|
|
||||||
|
|
||||||
local last_seek_time = nil
|
|
||||||
|
|
||||||
local effective_w = options.max_width
|
|
||||||
local effective_h = options.max_height
|
|
||||||
local real_w = nil
|
|
||||||
local real_h = nil
|
|
||||||
local last_real_w = nil
|
|
||||||
local last_real_h = nil
|
|
||||||
|
|
||||||
local script_name = nil
|
|
||||||
|
|
||||||
local show_thumbnail = false
|
|
||||||
|
|
||||||
local filters_reset = { ["lavfi-crop"] = true, ["crop"] = true }
|
|
||||||
local filters_runtime = { ["hflip"] = true, ["vflip"] = true }
|
|
||||||
local filters_all = { ["hflip"] = true, ["vflip"] = true, ["lavfi-crop"] = true, ["crop"] = true }
|
|
||||||
|
|
||||||
local tone_mappings = {
|
|
||||||
["none"] = true,
|
|
||||||
["clip"] = true,
|
|
||||||
["linear"] = true,
|
|
||||||
["gamma"] = true,
|
|
||||||
["reinhard"] = true,
|
|
||||||
["hable"] = true,
|
|
||||||
["mobius"] = true
|
|
||||||
}
|
|
||||||
local last_tone_mapping = nil
|
|
||||||
|
|
||||||
local last_vf_reset = ""
|
|
||||||
local last_vf_runtime = ""
|
|
||||||
|
|
||||||
local last_rotate = 0
|
|
||||||
|
|
||||||
local par = ""
|
|
||||||
local last_par = ""
|
|
||||||
|
|
||||||
local last_has_vid = 0
|
|
||||||
local has_vid = 0
|
|
||||||
|
|
||||||
local file_timer = nil
|
|
||||||
local file_check_period = 1 / 60
|
|
||||||
|
|
||||||
local allow_fast_seek = true
|
|
||||||
|
|
||||||
local client_script = [=[
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
|
||||||
trap "kill 0" EXIT
|
|
||||||
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
|
||||||
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
|
||||||
]=]
|
|
||||||
|
|
||||||
local function get_os()
|
|
||||||
local raw_os_name = ""
|
|
||||||
|
|
||||||
if jit and jit.os and jit.arch then
|
|
||||||
raw_os_name = jit.os
|
|
||||||
else
|
|
||||||
if package.config:sub(1, 1) == "\\" then
|
|
||||||
-- Windows
|
|
||||||
local env_OS = os.getenv("OS")
|
|
||||||
if env_OS then
|
|
||||||
raw_os_name = env_OS
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raw_os_name = subprocess({ "uname", "-s" }).stdout
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
raw_os_name = (raw_os_name):lower()
|
|
||||||
|
|
||||||
local os_patterns = {
|
|
||||||
["windows"] = "windows",
|
|
||||||
["linux"] = "linux",
|
|
||||||
|
|
||||||
["osx"] = "darwin",
|
|
||||||
["mac"] = "darwin",
|
|
||||||
["darwin"] = "darwin",
|
|
||||||
|
|
||||||
["^mingw"] = "windows",
|
|
||||||
["^cygwin"] = "windows",
|
|
||||||
|
|
||||||
["bsd$"] = "darwin",
|
|
||||||
["sunos"] = "darwin"
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Default to linux
|
|
||||||
local str_os_name = "linux"
|
|
||||||
|
|
||||||
for pattern, name in pairs(os_patterns) do
|
|
||||||
if raw_os_name:match(pattern) then
|
|
||||||
str_os_name = name
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return str_os_name
|
|
||||||
end
|
|
||||||
|
|
||||||
local os_name = mp.get_property("platform") or get_os()
|
|
||||||
|
|
||||||
local path_separator = os_name == "windows" and "\\" or "/"
|
|
||||||
|
|
||||||
if options.socket == "" then
|
|
||||||
if os_name == "windows" then
|
|
||||||
options.socket = "thumbfast"
|
|
||||||
else
|
|
||||||
options.socket = "/tmp/thumbfast"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if options.thumbnail == "" then
|
|
||||||
if os_name == "windows" then
|
|
||||||
options.thumbnail = os.getenv("TEMP") .. "\\thumbfast.out"
|
|
||||||
else
|
|
||||||
options.thumbnail = "/tmp/thumbfast.out"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local unique = mp.utils.getpid()
|
|
||||||
|
|
||||||
options.socket = options.socket .. unique
|
|
||||||
options.thumbnail = options.thumbnail .. unique
|
|
||||||
|
|
||||||
if options.direct_io then
|
|
||||||
if os_name == "windows" then
|
|
||||||
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
|
||||||
end
|
|
||||||
|
|
||||||
if winapi.socket_wc == "" then
|
|
||||||
options.direct_io = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local mpv_path = options.mpv_path
|
|
||||||
|
|
||||||
if mpv_path == "mpv" and os_name == "darwin" and unique then
|
|
||||||
-- TODO: look into ~~osxbundle/
|
|
||||||
mpv_path = string.gsub(subprocess({ "ps", "-o", "comm=", "-p", tostring(unique) }).stdout, "[\n\r]", "")
|
|
||||||
if mpv_path ~= "mpv" then
|
|
||||||
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
|
||||||
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
|
||||||
if mpv_bin and mpv_bin.is_file then
|
|
||||||
mpv_path = "/usr/local/mpv"
|
|
||||||
else
|
|
||||||
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
|
||||||
if mpv_app and mpv_app.is_file then
|
|
||||||
mp.msg.warn(
|
|
||||||
"symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
|
||||||
else
|
|
||||||
mp.msg.warn(
|
|
||||||
"drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function vo_tone_mapping()
|
|
||||||
local passes = mp.get_property_native("vo-passes")
|
|
||||||
if passes and passes["fresh"] then
|
|
||||||
for _, v in pairs(passes["fresh"]) do
|
|
||||||
for k2, v2 in pairs(v) do
|
|
||||||
if k2 == "desc" and v2 then
|
|
||||||
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
|
|
||||||
if tone_mapping then
|
|
||||||
return tone_mapping
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function vf_string(filters, full)
|
|
||||||
local vf = ""
|
|
||||||
local vf_table = properties["vf"]
|
|
||||||
|
|
||||||
if vf_table and #vf_table > 0 then
|
|
||||||
for i = #vf_table, 1, -1 do
|
|
||||||
if filters[vf_table[i].name] then
|
|
||||||
local args = ""
|
|
||||||
for key, value in pairs(vf_table[i].params) do
|
|
||||||
if args ~= "" then
|
|
||||||
args = args .. ":"
|
|
||||||
end
|
|
||||||
args = args .. key .. "=" .. value
|
|
||||||
end
|
|
||||||
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
|
|
||||||
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
|
|
||||||
local tone_mapping = options.tone_mapping
|
|
||||||
if tone_mapping == "auto" then
|
|
||||||
tone_mapping = last_tone_mapping or properties["tone-mapping"]
|
|
||||||
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
|
|
||||||
tone_mapping = vo_tone_mapping()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not tone_mappings[tone_mapping] then
|
|
||||||
tone_mapping = "hable"
|
|
||||||
end
|
|
||||||
last_tone_mapping = tone_mapping
|
|
||||||
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap=" .. tone_mapping .. ",zscale=transfer=bt709,"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if full then
|
|
||||||
vf = vf ..
|
|
||||||
"scale=w=" ..
|
|
||||||
effective_w .. ":h=" .. effective_h .. par ..
|
|
||||||
",pad=w=" .. effective_w .. ":h=" .. effective_h .. ":x=-1:y=-1,format=bgra"
|
|
||||||
end
|
|
||||||
|
|
||||||
return vf
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calc_dimensions()
|
|
||||||
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
|
||||||
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
|
||||||
if not width or not height then return end
|
|
||||||
|
|
||||||
local scale = properties["display-hidpi-scale"] or 1
|
|
||||||
|
|
||||||
if width / height > options.max_width / options.max_height then
|
|
||||||
effective_w = math.floor(options.max_width * scale + 0.5)
|
|
||||||
effective_h = math.floor(height / width * effective_w + 0.5)
|
|
||||||
else
|
|
||||||
effective_h = math.floor(options.max_height * scale + 0.5)
|
|
||||||
effective_w = math.floor(width / height * effective_h + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
|
|
||||||
if v_par == 1 then
|
|
||||||
par = ":force_original_aspect_ratio=decrease"
|
|
||||||
else
|
|
||||||
par = ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local info_timer = nil
|
|
||||||
|
|
||||||
local function info(w, h)
|
|
||||||
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
|
|
||||||
local image = properties["current-tracks"] and properties["current-tracks"]["video"] and
|
|
||||||
properties["current-tracks"]["video"]["image"]
|
|
||||||
local albumart = image and properties["current-tracks"]["video"]["albumart"]
|
|
||||||
|
|
||||||
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
|
||||||
has_vid == 0 or
|
|
||||||
(properties["demuxer-via-network"] and not options.network) or
|
|
||||||
(albumart and not options.audio) or
|
|
||||||
(image and not albumart) or
|
|
||||||
force_disabled
|
|
||||||
|
|
||||||
if info_timer then
|
|
||||||
info_timer:kill()
|
|
||||||
info_timer = nil
|
|
||||||
elseif has_vid == 0 or (rotate == nil and not disabled) then
|
|
||||||
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local json, _ = mp.utils.format_json({
|
|
||||||
width = w,
|
|
||||||
height = h,
|
|
||||||
disabled = disabled,
|
|
||||||
available = true,
|
|
||||||
socket = options.socket,
|
|
||||||
thumbnail = options.thumbnail,
|
|
||||||
overlay_id = options.overlay_id
|
|
||||||
})
|
|
||||||
if pre_0_30_0 then
|
|
||||||
mp.command_native({ "script-message", "thumbfast-info", json })
|
|
||||||
else
|
|
||||||
mp.command_native_async({ "script-message", "thumbfast-info", json }, function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function remove_thumbnail_files()
|
|
||||||
if file then
|
|
||||||
file:close()
|
|
||||||
file = nil
|
|
||||||
file_bytes = 0
|
|
||||||
end
|
|
||||||
os.remove(options.thumbnail)
|
|
||||||
os.remove(options.thumbnail .. ".bgra")
|
|
||||||
end
|
|
||||||
|
|
||||||
local activity_timer
|
|
||||||
|
|
||||||
local function spawn(time)
|
|
||||||
if disabled then return end
|
|
||||||
|
|
||||||
local path = properties["path"]
|
|
||||||
if path == nil then return end
|
|
||||||
|
|
||||||
if options.quit_after_inactivity > 0 then
|
|
||||||
if show_thumbnail or activity_timer:is_enabled() then
|
|
||||||
activity_timer:kill()
|
|
||||||
end
|
|
||||||
activity_timer:resume()
|
|
||||||
end
|
|
||||||
|
|
||||||
local open_filename = properties["stream-open-filename"]
|
|
||||||
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
|
|
||||||
if ytdl then
|
|
||||||
path = open_filename
|
|
||||||
end
|
|
||||||
|
|
||||||
remove_thumbnail_files()
|
|
||||||
|
|
||||||
local vid = properties["vid"]
|
|
||||||
has_vid = vid or 0
|
|
||||||
|
|
||||||
local args = {
|
|
||||||
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet",
|
|
||||||
"--no-terminal",
|
|
||||||
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no",
|
|
||||||
"--load-auto-profiles=no",
|
|
||||||
"--edition=" .. (properties["edition"] or "auto"), "--vid=" .. (vid or "auto"), "--no-sub", "--no-audio",
|
|
||||||
"--start=" .. time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
|
|
||||||
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
|
||||||
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2",
|
|
||||||
"--hwdec=" .. (options.hwdec and "auto" or "no"),
|
|
||||||
"--vf=" .. vf_string(filters_all, true),
|
|
||||||
"--sws-scaler=fast-bilinear",
|
|
||||||
"--video-rotate=" .. last_rotate,
|
|
||||||
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o=" .. options.thumbnail
|
|
||||||
}
|
|
||||||
|
|
||||||
if not pre_0_30_0 then
|
|
||||||
table.insert(args, "--sws-allow-zimg=no")
|
|
||||||
end
|
|
||||||
|
|
||||||
if os_name == "darwin" and properties["macos-app-activation-policy"] then
|
|
||||||
table.insert(args, "--macos-app-activation-policy=accessory")
|
|
||||||
end
|
|
||||||
|
|
||||||
if os_name == "windows" or pre_0_33_0 then
|
|
||||||
table.insert(args, "--input-ipc-server=" .. options.socket)
|
|
||||||
elseif not script_written then
|
|
||||||
local client_script_path = options.socket .. ".run"
|
|
||||||
local script = io.open(client_script_path, "w+")
|
|
||||||
if script == nil then
|
|
||||||
mp.msg.error("client script write failed")
|
|
||||||
return
|
|
||||||
else
|
|
||||||
script_written = true
|
|
||||||
script:write(string.format(client_script, options.socket))
|
|
||||||
script:close()
|
|
||||||
subprocess({ "chmod", "+x", client_script_path }, true)
|
|
||||||
table.insert(args, "--scripts=" .. client_script_path)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local client_script_path = options.socket .. ".run"
|
|
||||||
table.insert(args, "--scripts=" .. client_script_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(args, "--")
|
|
||||||
table.insert(args, path)
|
|
||||||
|
|
||||||
spawned = true
|
|
||||||
spawn_waiting = true
|
|
||||||
|
|
||||||
subprocess(args, true,
|
|
||||||
function(success, result)
|
|
||||||
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
|
|
||||||
spawned = false
|
|
||||||
spawn_waiting = false
|
|
||||||
options.tone_mapping = "no"
|
|
||||||
mp.msg.error("mpv subprocess create failed")
|
|
||||||
if not spawn_working then -- notify users of required configuration
|
|
||||||
if options.mpv_path == "mpv" then
|
|
||||||
if properties["current-vo"] == "libmpv" then
|
|
||||||
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
|
|
||||||
mpv_path = "ImPlay"
|
|
||||||
spawn(time)
|
|
||||||
else -- ImPlay not in path
|
|
||||||
if os_name ~= "darwin" then
|
|
||||||
force_disabled = true
|
|
||||||
info(real_w or effective_w, real_h or effective_h)
|
|
||||||
end
|
|
||||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
|
||||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup",
|
|
||||||
"Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" ..
|
|
||||||
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }),
|
|
||||||
"[/\\]", path_separator) .. "\nand restart ImPlay")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
|
||||||
if os_name == "windows" then
|
|
||||||
mp.commandv("script-message-to", "mpvnet", "show-text",
|
|
||||||
"thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
|
||||||
mp.commandv("script-message", "mpv.net", "show-text",
|
|
||||||
"thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
|
||||||
-- found ImPlay but not defined in config
|
|
||||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast",
|
|
||||||
"Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" ..
|
|
||||||
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]",
|
|
||||||
path_separator) .. "\nand restart ImPlay")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif success == true and (result.status == 0 or result.status == -2) then
|
|
||||||
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
|
|
||||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup",
|
|
||||||
"Set mpv_path=ImPlay in thumbfast config:\n" ..
|
|
||||||
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]",
|
|
||||||
path_separator) .. "\nand restart ImPlay")
|
|
||||||
end
|
|
||||||
spawn_working = true
|
|
||||||
spawn_waiting = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run(command)
|
|
||||||
if not spawned then return end
|
|
||||||
|
|
||||||
if options.direct_io then
|
|
||||||
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING,
|
|
||||||
winapi._createfile_pipe_flags, nil)
|
|
||||||
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
|
||||||
local buf = command .. "\n"
|
|
||||||
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
|
||||||
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
|
||||||
winapi.C.CloseHandle(hPipe)
|
|
||||||
end
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local command_n = command .. "\n"
|
|
||||||
|
|
||||||
if os_name == "windows" then
|
|
||||||
if file and file_bytes + #command_n >= 4096 then
|
|
||||||
file:close()
|
|
||||||
file = nil
|
|
||||||
file_bytes = 0
|
|
||||||
end
|
|
||||||
if not file then
|
|
||||||
file = io.open("\\\\.\\pipe\\" .. options.socket, "r+b")
|
|
||||||
end
|
|
||||||
elseif pre_0_33_0 then
|
|
||||||
subprocess({ "/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket })
|
|
||||||
return
|
|
||||||
elseif not file then
|
|
||||||
file = io.open(options.socket, "r+")
|
|
||||||
end
|
|
||||||
if file then
|
|
||||||
file_bytes = file:seek("end")
|
|
||||||
file:write(command_n)
|
|
||||||
file:flush()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function draw(w, h, script)
|
|
||||||
if not w or not show_thumbnail then return end
|
|
||||||
if x ~= nil then
|
|
||||||
if pre_0_30_0 then
|
|
||||||
mp.command_native({ "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h,
|
|
||||||
(4 * w) })
|
|
||||||
else
|
|
||||||
mp.command_native_async(
|
|
||||||
{ "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h, (4 * w) },
|
|
||||||
function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
elseif script then
|
|
||||||
local json, _ = mp.utils.format_json({
|
|
||||||
width = w,
|
|
||||||
height = h,
|
|
||||||
x = x,
|
|
||||||
y = y,
|
|
||||||
socket = options.socket,
|
|
||||||
thumbnail = options.thumbnail,
|
|
||||||
overlay_id = options.overlay_id
|
|
||||||
})
|
|
||||||
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function real_res(req_w, req_h, filesize)
|
|
||||||
local count = filesize / 4
|
|
||||||
local diff = (req_w * req_h) - count
|
|
||||||
|
|
||||||
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
|
|
||||||
req_w, req_h = req_h, req_w
|
|
||||||
end
|
|
||||||
|
|
||||||
if diff == 0 then
|
|
||||||
return req_w, req_h
|
|
||||||
else
|
|
||||||
local threshold = 5 -- throw out results that change too much
|
|
||||||
local long_side, short_side = req_w, req_h
|
|
||||||
if req_h > req_w then
|
|
||||||
long_side, short_side = req_h, req_w
|
|
||||||
end
|
|
||||||
for a = short_side, short_side - threshold, -1 do
|
|
||||||
if count % a == 0 then
|
|
||||||
local b = count / a
|
|
||||||
if long_side - b < threshold then
|
|
||||||
if req_h < req_w then return b, a else return a, b end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function move_file(from, to)
|
|
||||||
if os_name == "windows" then
|
|
||||||
os.remove(to)
|
|
||||||
end
|
|
||||||
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
|
||||||
os.rename(from, to)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function seek(fast)
|
|
||||||
if last_seek_time then
|
|
||||||
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local seek_period = 3 / 60
|
|
||||||
local seek_period_counter = 0
|
|
||||||
local seek_timer
|
|
||||||
seek_timer = mp.add_periodic_timer(seek_period, function()
|
|
||||||
if seek_period_counter == 0 then
|
|
||||||
seek(allow_fast_seek)
|
|
||||||
seek_period_counter = 1
|
|
||||||
else
|
|
||||||
if seek_period_counter == 2 then
|
|
||||||
if allow_fast_seek then
|
|
||||||
seek_timer:kill()
|
|
||||||
seek()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
seek_period_counter = seek_period_counter + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
seek_timer:kill()
|
|
||||||
|
|
||||||
local function request_seek()
|
|
||||||
if seek_timer:is_enabled() then
|
|
||||||
seek_period_counter = 0
|
|
||||||
else
|
|
||||||
seek_timer:resume()
|
|
||||||
seek(allow_fast_seek)
|
|
||||||
seek_period_counter = 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function check_new_thumb()
|
|
||||||
-- the slave might start writing to the file after checking existance and
|
|
||||||
-- validity but before actually moving the file, so move to a temporary
|
|
||||||
-- location before validity check to make sure everything stays consistant
|
|
||||||
-- and valid thumbnails don't get overwritten by invalid ones
|
|
||||||
local tmp = options.thumbnail .. ".tmp"
|
|
||||||
move_file(options.thumbnail, tmp)
|
|
||||||
local finfo = mp.utils.file_info(tmp)
|
|
||||||
if not finfo then return false end
|
|
||||||
spawn_waiting = false
|
|
||||||
local w, h = real_res(effective_w, effective_h, finfo.size)
|
|
||||||
if w then -- only accept valid thumbnails
|
|
||||||
move_file(tmp, options.thumbnail .. ".bgra")
|
|
||||||
|
|
||||||
real_w, real_h = w, h
|
|
||||||
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
|
||||||
last_real_w, last_real_h = real_w, real_h
|
|
||||||
info(real_w, real_h)
|
|
||||||
end
|
|
||||||
if not show_thumbnail then
|
|
||||||
file_timer:kill()
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
file_timer = mp.add_periodic_timer(file_check_period, function()
|
|
||||||
if check_new_thumb() then
|
|
||||||
draw(real_w, real_h, script_name)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
file_timer:kill()
|
|
||||||
|
|
||||||
local function clear()
|
|
||||||
file_timer:kill()
|
|
||||||
seek_timer:kill()
|
|
||||||
if options.quit_after_inactivity > 0 then
|
|
||||||
if show_thumbnail or activity_timer:is_enabled() then
|
|
||||||
activity_timer:kill()
|
|
||||||
end
|
|
||||||
activity_timer:resume()
|
|
||||||
end
|
|
||||||
last_seek_time = nil
|
|
||||||
show_thumbnail = false
|
|
||||||
last_x = nil
|
|
||||||
last_y = nil
|
|
||||||
if script_name then return end
|
|
||||||
if pre_0_30_0 then
|
|
||||||
mp.command_native({ "overlay-remove", options.overlay_id })
|
|
||||||
else
|
|
||||||
mp.command_native_async({ "overlay-remove", options.overlay_id }, function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function quit()
|
|
||||||
activity_timer:kill()
|
|
||||||
if show_thumbnail then
|
|
||||||
activity_timer:resume()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
run("quit")
|
|
||||||
spawned = false
|
|
||||||
real_w, real_h = nil, nil
|
|
||||||
clear()
|
|
||||||
end
|
|
||||||
|
|
||||||
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
|
|
||||||
activity_timer:kill()
|
|
||||||
|
|
||||||
local function thumb(time, r_x, r_y, script)
|
|
||||||
if disabled then return end
|
|
||||||
|
|
||||||
time = tonumber(time)
|
|
||||||
if time == nil then return end
|
|
||||||
|
|
||||||
if r_x == "" or r_y == "" then
|
|
||||||
x, y = nil, nil
|
|
||||||
else
|
|
||||||
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
script_name = script
|
|
||||||
if last_x ~= x or last_y ~= y or not show_thumbnail then
|
|
||||||
show_thumbnail = true
|
|
||||||
last_x = x
|
|
||||||
last_y = y
|
|
||||||
draw(real_w, real_h, script)
|
|
||||||
end
|
|
||||||
|
|
||||||
if options.quit_after_inactivity > 0 then
|
|
||||||
if show_thumbnail or activity_timer:is_enabled() then
|
|
||||||
activity_timer:kill()
|
|
||||||
end
|
|
||||||
activity_timer:resume()
|
|
||||||
end
|
|
||||||
|
|
||||||
if time == last_seek_time then return end
|
|
||||||
last_seek_time = time
|
|
||||||
if not spawned then spawn(time) end
|
|
||||||
request_seek()
|
|
||||||
if not file_timer:is_enabled() then file_timer:resume() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function watch_changes()
|
|
||||||
if not dirty or not properties["video-out-params"] then return end
|
|
||||||
dirty = false
|
|
||||||
|
|
||||||
local old_w = effective_w
|
|
||||||
local old_h = effective_h
|
|
||||||
|
|
||||||
calc_dimensions()
|
|
||||||
|
|
||||||
local vf_reset = vf_string(filters_reset)
|
|
||||||
local rotate = properties["video-rotate"] or 0
|
|
||||||
|
|
||||||
local resized = old_w ~= effective_w or
|
|
||||||
old_h ~= effective_h or
|
|
||||||
last_vf_reset ~= vf_reset or
|
|
||||||
(last_rotate % 180) ~= (rotate % 180) or
|
|
||||||
par ~= last_par
|
|
||||||
|
|
||||||
if resized then
|
|
||||||
last_rotate = rotate
|
|
||||||
info(effective_w, effective_h)
|
|
||||||
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
|
|
||||||
info(effective_w, effective_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
if spawned then
|
|
||||||
if resized then
|
|
||||||
-- mpv doesn't allow us to change output size
|
|
||||||
local seek_time = last_seek_time
|
|
||||||
run("quit")
|
|
||||||
clear()
|
|
||||||
spawned = false
|
|
||||||
spawn(seek_time or mp.get_property_number("time-pos", 0))
|
|
||||||
file_timer:resume()
|
|
||||||
else
|
|
||||||
if rotate ~= last_rotate then
|
|
||||||
run("set video-rotate " .. rotate)
|
|
||||||
end
|
|
||||||
local vf_runtime = vf_string(filters_runtime)
|
|
||||||
if vf_runtime ~= last_vf_runtime then
|
|
||||||
run("vf set " .. vf_string(filters_all, true))
|
|
||||||
last_vf_runtime = vf_runtime
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
last_vf_runtime = vf_string(filters_runtime)
|
|
||||||
end
|
|
||||||
|
|
||||||
last_vf_reset = vf_reset
|
|
||||||
last_rotate = rotate
|
|
||||||
last_par = par
|
|
||||||
last_has_vid = has_vid
|
|
||||||
|
|
||||||
if not spawned and not disabled and options.spawn_first and resized then
|
|
||||||
spawn(mp.get_property_number("time-pos", 0))
|
|
||||||
file_timer:resume()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update_property(name, value)
|
|
||||||
properties[name] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update_property_dirty(name, value)
|
|
||||||
properties[name] = value
|
|
||||||
dirty = true
|
|
||||||
if name == "tone-mapping" then
|
|
||||||
last_tone_mapping = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update_tracklist(_, value)
|
|
||||||
-- current-tracks shim
|
|
||||||
for _, track in ipairs(value) do
|
|
||||||
if track.type == "video" and track.selected then
|
|
||||||
properties["current-tracks/video/image"] = track.image
|
|
||||||
properties["current-tracks/video/albumart"] = track.albumart
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sync_changes(prop, val)
|
|
||||||
update_property(prop, val)
|
|
||||||
if val == nil then return end
|
|
||||||
|
|
||||||
if type(val) == "boolean" then
|
|
||||||
if prop == "vid" then
|
|
||||||
has_vid = 0
|
|
||||||
last_has_vid = 0
|
|
||||||
info(effective_w, effective_h)
|
|
||||||
clear()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
val = val and "yes" or "no"
|
|
||||||
end
|
|
||||||
|
|
||||||
if prop == "vid" then
|
|
||||||
has_vid = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if not spawned then return end
|
|
||||||
|
|
||||||
run("set " .. prop .. " " .. val)
|
|
||||||
dirty = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function file_load()
|
|
||||||
clear()
|
|
||||||
spawned = false
|
|
||||||
real_w, real_h = nil, nil
|
|
||||||
last_real_w, last_real_h = nil, nil
|
|
||||||
last_tone_mapping = nil
|
|
||||||
last_seek_time = nil
|
|
||||||
if info_timer then
|
|
||||||
info_timer:kill()
|
|
||||||
info_timer = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
calc_dimensions()
|
|
||||||
info(effective_w, effective_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shutdown()
|
|
||||||
run("quit")
|
|
||||||
remove_thumbnail_files()
|
|
||||||
if os_name ~= "windows" then
|
|
||||||
os.remove(options.socket)
|
|
||||||
os.remove(options.socket .. ".run")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function on_duration(_, val)
|
|
||||||
allow_fast_seek = (val or 30) >= 30
|
|
||||||
end
|
|
||||||
|
|
||||||
mp.observe_property("current-tracks", "native", function(name, value)
|
|
||||||
if pre_0_33_0 then
|
|
||||||
mp.unobserve_property(update_tracklist)
|
|
||||||
pre_0_33_0 = false
|
|
||||||
end
|
|
||||||
update_property(name, value)
|
|
||||||
end)
|
|
||||||
|
|
||||||
mp.observe_property("track-list", "native", update_tracklist)
|
|
||||||
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
|
|
||||||
mp.observe_property("video-out-params", "native", update_property_dirty)
|
|
||||||
mp.observe_property("video-params", "native", update_property_dirty)
|
|
||||||
mp.observe_property("vf", "native", update_property_dirty)
|
|
||||||
mp.observe_property("tone-mapping", "native", update_property_dirty)
|
|
||||||
mp.observe_property("demuxer-via-network", "native", update_property)
|
|
||||||
mp.observe_property("stream-open-filename", "native", update_property)
|
|
||||||
mp.observe_property("macos-app-activation-policy", "native", update_property)
|
|
||||||
mp.observe_property("current-vo", "native", update_property)
|
|
||||||
mp.observe_property("video-rotate", "native", update_property)
|
|
||||||
mp.observe_property("path", "native", update_property)
|
|
||||||
mp.observe_property("vid", "native", sync_changes)
|
|
||||||
mp.observe_property("edition", "native", sync_changes)
|
|
||||||
mp.observe_property("duration", "native", on_duration)
|
|
||||||
|
|
||||||
mp.register_script_message("thumb", thumb)
|
|
||||||
mp.register_script_message("clear", clear)
|
|
||||||
|
|
||||||
mp.register_event("file-loaded", file_load)
|
|
||||||
mp.register_event("shutdown", shutdown)
|
|
||||||
|
|
||||||
mp.register_idle(watch_changes)
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,37 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@class BufferingIndicator : Element
|
|
||||||
local BufferingIndicator = class(Element)
|
|
||||||
|
|
||||||
function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end
|
|
||||||
function BufferingIndicator:init()
|
|
||||||
Element.init(self, 'buffer_indicator')
|
|
||||||
self.ignores_menu = true
|
|
||||||
self.enabled = false
|
|
||||||
end
|
|
||||||
|
|
||||||
function BufferingIndicator:decide_enabled()
|
|
||||||
local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100
|
|
||||||
local player = state.core_idle and not state.eof_reached
|
|
||||||
if self.enabled then
|
|
||||||
if not player or (state.pause and not cache) then self.enabled = false end
|
|
||||||
elseif player and cache and state.uncached_ranges then self.enabled = true end
|
|
||||||
end
|
|
||||||
|
|
||||||
function BufferingIndicator:on_prop_pause() self:decide_enabled() end
|
|
||||||
function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end
|
|
||||||
function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end
|
|
||||||
function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end
|
|
||||||
function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end
|
|
||||||
function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end
|
|
||||||
|
|
||||||
function BufferingIndicator:render()
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3})
|
|
||||||
local size = round(30 + math.min(display.width, display.height) / 10)
|
|
||||||
local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8
|
|
||||||
ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity})
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return BufferingIndicator
|
|
|
@ -1,90 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string}
|
|
||||||
|
|
||||||
---@class Button : Element
|
|
||||||
local Button = class(Element)
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
---@param props ButtonProps
|
|
||||||
function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end
|
|
||||||
---@param id string
|
|
||||||
---@param props ButtonProps
|
|
||||||
function Button:init(id, props)
|
|
||||||
self.icon = props.icon
|
|
||||||
self.active = props.active
|
|
||||||
self.tooltip = props.tooltip
|
|
||||||
self.badge = props.badge
|
|
||||||
self.foreground = props.foreground or fg
|
|
||||||
self.background = props.background or bg
|
|
||||||
---@type fun()
|
|
||||||
self.on_click = props.on_click
|
|
||||||
Element.init(self, id, props)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end
|
|
||||||
function Button:handle_cursor_down()
|
|
||||||
-- We delay the callback to next tick, otherwise we are risking race
|
|
||||||
-- conditions as we are in the middle of event dispatching.
|
|
||||||
-- For example, handler might add a menu to the end of the element stack, and that
|
|
||||||
-- than picks up this click event we are in right now, and instantly closes itself.
|
|
||||||
mp.add_timeout(0.01, self.on_click)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Button:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
if visibility <= 0 then return end
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
cursor.on_primary_down = function() self:handle_cursor_down() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
local is_hover = self.proximity_raw == 0
|
|
||||||
local is_hover_or_active = is_hover or self.active
|
|
||||||
local foreground = self.active and self.background or self.foreground
|
|
||||||
local background = self.active and self.foreground or self.background
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
if is_hover_or_active then
|
|
||||||
ass:rect(self.ax, self.ay, self.bx, self.by, {
|
|
||||||
color = self.active and background or foreground, radius = 2,
|
|
||||||
opacity = visibility * (self.active and 1 or 0.3),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Tooltip on hover
|
|
||||||
if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end
|
|
||||||
|
|
||||||
-- Badge
|
|
||||||
local icon_clip
|
|
||||||
if self.badge then
|
|
||||||
local badge_font_size = self.font_size * 0.6
|
|
||||||
local badge_opts = {size = badge_font_size, color = background, opacity = visibility}
|
|
||||||
local badge_width = text_width(self.badge, badge_opts)
|
|
||||||
local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93)
|
|
||||||
local bx, by = self.bx - 1, self.by - 1
|
|
||||||
ass:rect(bx - width, by - height, bx, by, {
|
|
||||||
color = foreground, radius = 2, opacity = visibility,
|
|
||||||
border = self.active and 0 or 1, border_color = background,
|
|
||||||
})
|
|
||||||
ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts)
|
|
||||||
|
|
||||||
local clip_border = math.max(self.font_size / 20, 1)
|
|
||||||
local clip_path = assdraw.ass_new()
|
|
||||||
clip_path:round_rect_cw(
|
|
||||||
math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3
|
|
||||||
)
|
|
||||||
icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Icon
|
|
||||||
local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2)
|
|
||||||
ass:icon(x, y, self.font_size, self.icon, {
|
|
||||||
color = foreground, border = self.active and 0 or options.text_border, border_color = background,
|
|
||||||
opacity = visibility, clip = icon_clip,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Button
|
|
|
@ -1,329 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
local Button = require('uosc_shared/elements/Button')
|
|
||||||
local CycleButton = require('uosc_shared/elements/CycleButton')
|
|
||||||
local Speed = require('uosc_shared/elements/Speed')
|
|
||||||
|
|
||||||
-- `scale` - `options.controls_size` scale factor.
|
|
||||||
-- `ratio` - Width/height ratio of a static or dynamic element.
|
|
||||||
-- `ratio_min` Min ratio for 'dynamic' sized element.
|
|
||||||
---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table<string, boolean>}
|
|
||||||
|
|
||||||
---@class Controls : Element
|
|
||||||
local Controls = class(Element)
|
|
||||||
|
|
||||||
function Controls:new() return Class.new(self) --[[@as Controls]] end
|
|
||||||
function Controls:init()
|
|
||||||
Element.init(self, 'controls')
|
|
||||||
---@type ControlItem[] All control elements serialized from `options.controls`.
|
|
||||||
self.controls = {}
|
|
||||||
---@type ControlItem[] Only controls that match current dispositions.
|
|
||||||
self.layout = {}
|
|
||||||
|
|
||||||
-- Serialize control elements
|
|
||||||
local shorthands = {
|
|
||||||
menu = 'command:menu:script-binding uosc/menu-blurred?Menu',
|
|
||||||
subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles',
|
|
||||||
audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio',
|
|
||||||
['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
|
|
||||||
video = 'command:theaters:script-binding uosc/video#video>1?Video',
|
|
||||||
playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
|
|
||||||
chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters',
|
|
||||||
['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions',
|
|
||||||
['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality',
|
|
||||||
['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
|
|
||||||
['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files',
|
|
||||||
prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
|
|
||||||
next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
|
|
||||||
first = 'command:first_page:script-binding uosc/first?First',
|
|
||||||
last = 'command:last_page:script-binding uosc/last?Last',
|
|
||||||
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist',
|
|
||||||
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file',
|
|
||||||
shuffle = 'toggle:shuffle:shuffle?Shuffle',
|
|
||||||
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen',
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Parse out disposition/config pairs
|
|
||||||
local items = {}
|
|
||||||
local in_disposition = false
|
|
||||||
local current_item = nil
|
|
||||||
for c in options.controls:gmatch('.') do
|
|
||||||
if not current_item then current_item = {disposition = '', config = ''} end
|
|
||||||
if c == '<' and #current_item.config == 0 then in_disposition = true
|
|
||||||
elseif c == '>' and #current_item.config == 0 then in_disposition = false
|
|
||||||
elseif c == ',' and not in_disposition then
|
|
||||||
items[#items + 1] = current_item
|
|
||||||
current_item = nil
|
|
||||||
else
|
|
||||||
local prop = in_disposition and 'disposition' or 'config'
|
|
||||||
current_item[prop] = current_item[prop] .. c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
items[#items + 1] = current_item
|
|
||||||
|
|
||||||
-- Create controls
|
|
||||||
self.controls = {}
|
|
||||||
for i, item in ipairs(items) do
|
|
||||||
local config = shorthands[item.config] and shorthands[item.config] or item.config
|
|
||||||
local config_tooltip = split(config, ' *%? *')
|
|
||||||
local tooltip = config_tooltip[2]
|
|
||||||
config = shorthands[config_tooltip[1]]
|
|
||||||
and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1]
|
|
||||||
local config_badge = split(config, ' *# *')
|
|
||||||
config = config_badge[1]
|
|
||||||
local badge = config_badge[2]
|
|
||||||
local parts = split(config, ' *: *')
|
|
||||||
local kind, params = parts[1], itable_slice(parts, 2)
|
|
||||||
|
|
||||||
-- Serialize dispositions
|
|
||||||
local dispositions = {}
|
|
||||||
for _, definition in ipairs(split(item.disposition, ' *, *')) do
|
|
||||||
if #definition > 0 then
|
|
||||||
local value = definition:sub(1, 1) ~= '!'
|
|
||||||
local name = not value and definition:sub(2) or definition
|
|
||||||
local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name
|
|
||||||
dispositions[prop] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Convert toggles into cycles
|
|
||||||
if kind == 'toggle' then
|
|
||||||
kind = 'cycle'
|
|
||||||
params[#params + 1] = 'no/yes!'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Create a control element
|
|
||||||
local control = {dispositions = dispositions, kind = kind}
|
|
||||||
|
|
||||||
if kind == 'space' then
|
|
||||||
control.sizing = 'space'
|
|
||||||
elseif kind == 'gap' then
|
|
||||||
table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0})
|
|
||||||
elseif kind == 'command' then
|
|
||||||
if #params ~= 2 then
|
|
||||||
mp.error(string.format(
|
|
||||||
'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/')
|
|
||||||
))
|
|
||||||
else
|
|
||||||
local element = Button:new('control_' .. i, {
|
|
||||||
icon = params[1],
|
|
||||||
anchor_id = 'controls',
|
|
||||||
on_click = function() mp.command(params[2]) end,
|
|
||||||
tooltip = tooltip,
|
|
||||||
count_prop = 'sub',
|
|
||||||
})
|
|
||||||
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
|
||||||
if badge then self:register_badge_updater(badge, element) end
|
|
||||||
end
|
|
||||||
elseif kind == 'cycle' then
|
|
||||||
if #params ~= 3 then
|
|
||||||
mp.error(string.format(
|
|
||||||
'cycle button needs 3 parameters, %d received: %s',
|
|
||||||
#params, table.concat(params, '/')
|
|
||||||
))
|
|
||||||
else
|
|
||||||
local state_configs = split(params[3], ' */ *')
|
|
||||||
local states = {}
|
|
||||||
|
|
||||||
for _, state_config in ipairs(state_configs) do
|
|
||||||
local active = false
|
|
||||||
if state_config:sub(-1) == '!' then
|
|
||||||
active = true
|
|
||||||
state_config = state_config:sub(1, -2)
|
|
||||||
end
|
|
||||||
local state_params = split(state_config, ' *= *')
|
|
||||||
local value, icon = state_params[1], state_params[2] or params[1]
|
|
||||||
states[#states + 1] = {value = value, icon = icon, active = active}
|
|
||||||
end
|
|
||||||
|
|
||||||
local element = CycleButton:new('control_' .. i, {
|
|
||||||
prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip,
|
|
||||||
})
|
|
||||||
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
|
|
||||||
if badge then self:register_badge_updater(badge, element) end
|
|
||||||
end
|
|
||||||
elseif kind == 'speed' then
|
|
||||||
if not Elements.speed then
|
|
||||||
local element = Speed:new({anchor_id = 'controls'})
|
|
||||||
table_assign(control, {
|
|
||||||
element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2,
|
|
||||||
})
|
|
||||||
else
|
|
||||||
msg.error('there can only be 1 speed slider')
|
|
||||||
end
|
|
||||||
else
|
|
||||||
msg.error('unknown element kind "' .. kind .. '"')
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
self.controls[#self.controls + 1] = control
|
|
||||||
end
|
|
||||||
|
|
||||||
self:reflow()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Controls:reflow()
|
|
||||||
-- Populate the layout only with items that match current disposition
|
|
||||||
self.layout = {}
|
|
||||||
for _, control in ipairs(self.controls) do
|
|
||||||
local matches = true
|
|
||||||
for prop, value in pairs(control.dispositions) do
|
|
||||||
if state[prop] ~= value then
|
|
||||||
matches = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if control.element then control.element.enabled = matches end
|
|
||||||
if matches then self.layout[#self.layout + 1] = control end
|
|
||||||
end
|
|
||||||
|
|
||||||
self:update_dimensions()
|
|
||||||
Elements:trigger('controls_reflow')
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param badge string
|
|
||||||
---@param element Element An element that supports `badge` property.
|
|
||||||
function Controls:register_badge_updater(badge, element)
|
|
||||||
local prop_and_limit = split(badge, ' *> *')
|
|
||||||
local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1)
|
|
||||||
local observable_name, serializer, is_external_prop = prop, nil, false
|
|
||||||
|
|
||||||
if itable_index_of({'sub', 'audio', 'video'}, prop) then
|
|
||||||
observable_name = 'track-list'
|
|
||||||
serializer = function(value)
|
|
||||||
local count = 0
|
|
||||||
for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local parts = split(prop, '@')
|
|
||||||
-- Support both new `prop@owner` and old `@prop` syntaxes
|
|
||||||
if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end
|
|
||||||
serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handler(_, value)
|
|
||||||
local new_value = serializer(value) --[[@as nil|string|integer]]
|
|
||||||
local value_number = tonumber(new_value)
|
|
||||||
if value_number then new_value = value_number > limit and value_number or nil end
|
|
||||||
element.badge = new_value
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
|
|
||||||
else mp.observe_property(observable_name, 'native', handler) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Controls:get_visibility()
|
|
||||||
return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered()
|
|
||||||
and -1 or Element.get_visibility(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Controls:update_dimensions()
|
|
||||||
local window_border = Elements.window_border.size
|
|
||||||
local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size
|
|
||||||
local spacing = options.controls_spacing
|
|
||||||
local margin = options.controls_margin
|
|
||||||
|
|
||||||
-- Disable when not enough space
|
|
||||||
local available_space = display.height - Elements.window_border.size * 2
|
|
||||||
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
|
|
||||||
if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end
|
|
||||||
self.enabled = available_space > size + 10
|
|
||||||
|
|
||||||
-- Reset hide/enabled flags
|
|
||||||
for c, control in ipairs(self.layout) do
|
|
||||||
control.hide = false
|
|
||||||
if control.element then control.element.enabled = self.enabled end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self.enabled then return end
|
|
||||||
|
|
||||||
-- Container
|
|
||||||
self.bx = display.width - window_border - margin
|
|
||||||
self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin
|
|
||||||
self.ax, self.ay = window_border + margin, self.by - size
|
|
||||||
|
|
||||||
-- Controls
|
|
||||||
local available_width = self.bx - self.ax
|
|
||||||
local statics_width = (#self.layout - 1) * spacing
|
|
||||||
local min_content_width = statics_width
|
|
||||||
local max_dynamics_width, dynamic_units, spaces = 0, 0, 0
|
|
||||||
|
|
||||||
-- Calculate statics_width, min_content_width, and count spaces
|
|
||||||
for c, control in ipairs(self.layout) do
|
|
||||||
if control.sizing == 'space' then
|
|
||||||
spaces = spaces + 1
|
|
||||||
elseif control.sizing == 'static' then
|
|
||||||
local width = size * control.scale * control.ratio
|
|
||||||
statics_width = statics_width + width
|
|
||||||
min_content_width = min_content_width + width
|
|
||||||
elseif control.sizing == 'dynamic' then
|
|
||||||
min_content_width = min_content_width + size * control.scale * control.ratio_min
|
|
||||||
max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio
|
|
||||||
dynamic_units = dynamic_units + control.scale * control.ratio
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hide & disable elements in the middle until we fit into available width
|
|
||||||
if min_content_width > available_width then
|
|
||||||
local i = math.ceil(#self.layout / 2 + 0.1)
|
|
||||||
for a = 0, #self.layout - 1, 1 do
|
|
||||||
i = i + (a * (a % 2 == 0 and 1 or -1))
|
|
||||||
local control = self.layout[i]
|
|
||||||
|
|
||||||
if control.kind ~= 'gap' and control.kind ~= 'space' then
|
|
||||||
control.hide = true
|
|
||||||
if control.element then control.element.enabled = false end
|
|
||||||
if control.sizing == 'static' then
|
|
||||||
local width = size * control.scale * control.ratio
|
|
||||||
min_content_width = min_content_width - width - spacing
|
|
||||||
statics_width = statics_width - width - spacing
|
|
||||||
elseif control.sizing == 'dynamic' then
|
|
||||||
min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing
|
|
||||||
max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio
|
|
||||||
dynamic_units = dynamic_units - control.scale * control.ratio
|
|
||||||
end
|
|
||||||
|
|
||||||
if min_content_width < available_width then break end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Lay out the elements
|
|
||||||
local current_x = self.ax
|
|
||||||
local width_for_dynamics = available_width - statics_width
|
|
||||||
local space_width = (width_for_dynamics - max_dynamics_width) / spaces
|
|
||||||
|
|
||||||
for c, control in ipairs(self.layout) do
|
|
||||||
if not control.hide then
|
|
||||||
local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio
|
|
||||||
local width, height = 0, 0
|
|
||||||
|
|
||||||
if sizing == 'space' then
|
|
||||||
if space_width > 0 then width = space_width end
|
|
||||||
elseif sizing == 'static' then
|
|
||||||
height = size * scale
|
|
||||||
width = height * ratio
|
|
||||||
elseif sizing == 'dynamic' then
|
|
||||||
height = size * scale
|
|
||||||
width = max_dynamics_width < width_for_dynamics
|
|
||||||
and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units)
|
|
||||||
end
|
|
||||||
|
|
||||||
local bx = current_x + width
|
|
||||||
if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end
|
|
||||||
current_x = bx + spacing
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Elements:update_proximities()
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Controls:on_dispositions() self:reflow() end
|
|
||||||
function Controls:on_display() self:update_dimensions() end
|
|
||||||
function Controls:on_prop_border() self:update_dimensions() end
|
|
||||||
function Controls:on_prop_fullormaxed() self:update_dimensions() end
|
|
||||||
function Controls:on_timeline_enabled() self:update_dimensions() end
|
|
||||||
|
|
||||||
return Controls
|
|
|
@ -1,35 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@class Curtain : Element
|
|
||||||
local Curtain = class(Element)
|
|
||||||
|
|
||||||
function Curtain:new() return Class.new(self) --[[@as Curtain]] end
|
|
||||||
function Curtain:init()
|
|
||||||
Element.init(self, 'curtain', {ignores_menu = true})
|
|
||||||
self.opacity = 0
|
|
||||||
---@type string[]
|
|
||||||
self.dependents = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
function Curtain:register(id)
|
|
||||||
self.dependents[#self.dependents + 1] = id
|
|
||||||
if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
function Curtain:unregister(id)
|
|
||||||
self.dependents = itable_filter(self.dependents, function(item) return item ~= id end)
|
|
||||||
if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Curtain:render()
|
|
||||||
if self.opacity == 0 or options.curtain_opacity == 0 then return end
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
ass:rect(0, 0, display.width, display.height, {
|
|
||||||
color = '000000', opacity = options.curtain_opacity * self.opacity,
|
|
||||||
})
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Curtain
|
|
|
@ -1,64 +0,0 @@
|
||||||
local Button = require('uosc_shared/elements/Button')
|
|
||||||
|
|
||||||
---@alias CycleState {value: any; icon: string; active?: boolean}
|
|
||||||
---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
|
|
||||||
|
|
||||||
---@class CycleButton : Button
|
|
||||||
local CycleButton = class(Button)
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
---@param props CycleButtonProps
|
|
||||||
function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end
|
|
||||||
---@param id string
|
|
||||||
---@param props CycleButtonProps
|
|
||||||
function CycleButton:init(id, props)
|
|
||||||
local is_state_prop = itable_index_of({'shuffle'}, props.prop)
|
|
||||||
self.prop = props.prop
|
|
||||||
self.states = props.states
|
|
||||||
|
|
||||||
Button.init(self, id, props)
|
|
||||||
|
|
||||||
self.icon = self.states[1].icon
|
|
||||||
self.active = self.states[1].active
|
|
||||||
self.current_state_index = 1
|
|
||||||
self.on_click = function()
|
|
||||||
local new_state = self.states[self.current_state_index + 1] or self.states[1]
|
|
||||||
local new_value = new_state.value
|
|
||||||
if self.owner then
|
|
||||||
mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value)
|
|
||||||
elseif is_state_prop then
|
|
||||||
if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end
|
|
||||||
set_state(self.prop, new_value)
|
|
||||||
else
|
|
||||||
mp.set_property(self.prop, new_value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.handle_change = function(name, value)
|
|
||||||
if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end
|
|
||||||
local index = itable_find(self.states, function(state) return state.value == value end)
|
|
||||||
self.current_state_index = index or 1
|
|
||||||
self.icon = self.states[self.current_state_index].icon
|
|
||||||
self.active = self.states[self.current_state_index].active
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
local prop_parts = split(self.prop, '@')
|
|
||||||
if #prop_parts == 2 then -- External prop with a script owner
|
|
||||||
self.prop, self.owner = prop_parts[1], prop_parts[2]
|
|
||||||
self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end
|
|
||||||
self.handle_change(self.prop, external[self.prop])
|
|
||||||
elseif is_state_prop then -- uosc's state props
|
|
||||||
self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end
|
|
||||||
self.handle_change(self.prop, state[self.prop])
|
|
||||||
else
|
|
||||||
mp.observe_property(self.prop, 'string', self.handle_change)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function CycleButton:destroy()
|
|
||||||
Button.destroy(self)
|
|
||||||
mp.unobserve_property(self.handle_change)
|
|
||||||
end
|
|
||||||
|
|
||||||
return CycleButton
|
|
|
@ -1,154 +0,0 @@
|
||||||
---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}
|
|
||||||
|
|
||||||
-- Base class all elements inherit from.
|
|
||||||
---@class Element : Class
|
|
||||||
local Element = class()
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
---@param props? ElementProps
|
|
||||||
function Element:init(id, props)
|
|
||||||
self.id = id
|
|
||||||
-- `false` means element won't be rendered, or receive events
|
|
||||||
self.enabled = true
|
|
||||||
-- Element coordinates
|
|
||||||
self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0
|
|
||||||
-- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range.
|
|
||||||
self.proximity = 0
|
|
||||||
-- Raw proximity in pixels.
|
|
||||||
self.proximity_raw = INFINITY
|
|
||||||
---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility.
|
|
||||||
self.min_visibility = 0
|
|
||||||
---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations
|
|
||||||
self.forced_visibility = nil
|
|
||||||
---@type boolean Render this element even when menu is open.
|
|
||||||
self.ignores_menu = false
|
|
||||||
---@type nil|string ID of an element from which this one should inherit visibility.
|
|
||||||
self.anchor_id = nil
|
|
||||||
|
|
||||||
if props then table_assign(self, props) end
|
|
||||||
|
|
||||||
-- Flash timer
|
|
||||||
self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
|
|
||||||
local function getTo() return self.proximity end
|
|
||||||
local function onTweenEnd() self.forced_visibility = nil end
|
|
||||||
if self.enabled then self:tween_property('forced_visibility', 1, getTo, onTweenEnd)
|
|
||||||
else onTweenEnd() end
|
|
||||||
end)
|
|
||||||
self._flash_out_timer:kill()
|
|
||||||
|
|
||||||
Elements:add(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:destroy()
|
|
||||||
self.destroyed = true
|
|
||||||
Elements:remove(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:reset_proximity() self.proximity, self.proximity_raw = 0, INFINITY end
|
|
||||||
|
|
||||||
---@param ax number
|
|
||||||
---@param ay number
|
|
||||||
---@param bx number
|
|
||||||
---@param by number
|
|
||||||
function Element:set_coordinates(ax, ay, bx, by)
|
|
||||||
self.ax, self.ay, self.bx, self.by = ax, ay, bx, by
|
|
||||||
Elements:update_proximities()
|
|
||||||
self:maybe('on_coordinates')
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:update_proximity()
|
|
||||||
if cursor.hidden then
|
|
||||||
self:reset_proximity()
|
|
||||||
else
|
|
||||||
local range = options.proximity_out - options.proximity_in
|
|
||||||
self.proximity_raw = get_point_to_rectangle_proximity(cursor, self)
|
|
||||||
self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:is_persistent()
|
|
||||||
local persist = config[self.id .. '_persistency']
|
|
||||||
return persist and (
|
|
||||||
(persist.audio and state.is_audio)
|
|
||||||
or (persist.paused and state.pause and (not Elements.timeline.pressed or Elements.timeline.pressed.pause))
|
|
||||||
or (persist.video and state.is_video)
|
|
||||||
or (persist.image and state.is_image)
|
|
||||||
or (persist.idle and state.is_idle)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Decide elements visibility based on proximity and various other factors
|
|
||||||
function Element:get_visibility()
|
|
||||||
-- Hide when menu is open, unless this is a menu
|
|
||||||
---@diagnostic disable-next-line: undefined-global
|
|
||||||
if not self.ignores_menu and Menu and Menu:is_open() then return 0 end
|
|
||||||
|
|
||||||
-- Persistency
|
|
||||||
if self:is_persistent() then return 1 end
|
|
||||||
|
|
||||||
-- Forced visibility
|
|
||||||
if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end
|
|
||||||
|
|
||||||
-- Anchor inheritance
|
|
||||||
-- If anchor returns -1, it means all attached elements should force hide.
|
|
||||||
local anchor = self.anchor_id and Elements[self.anchor_id]
|
|
||||||
local anchor_visibility = anchor and anchor:get_visibility() or 0
|
|
||||||
|
|
||||||
return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Call method if it exists
|
|
||||||
function Element:maybe(name, ...)
|
|
||||||
if self[name] then return self[name](self, ...) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Attach a tweening animation to this element
|
|
||||||
---@param from number
|
|
||||||
---@param to number|fun():number
|
|
||||||
---@param setter fun(value: number)
|
|
||||||
---@param factor_or_callback? number|fun()
|
|
||||||
---@param callback? fun() Called either on animation end, or when animation is killed.
|
|
||||||
function Element:tween(from, to, setter, factor_or_callback, callback)
|
|
||||||
self:tween_stop()
|
|
||||||
self._kill_tween = self.enabled and tween(
|
|
||||||
from, to, setter, factor_or_callback,
|
|
||||||
function()
|
|
||||||
self._kill_tween = nil
|
|
||||||
if callback then callback() end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Element:is_tweening() return self and self._kill_tween end
|
|
||||||
function Element:tween_stop() self:maybe('_kill_tween') end
|
|
||||||
|
|
||||||
-- Animate an element property between 2 values.
|
|
||||||
---@param prop string
|
|
||||||
---@param from number
|
|
||||||
---@param to number|fun():number
|
|
||||||
---@param factor_or_callback? number|fun()
|
|
||||||
---@param callback? fun() Called either on animation end, or when animation is killed.
|
|
||||||
function Element:tween_property(prop, from, to, factor_or_callback, callback)
|
|
||||||
self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
function Element:trigger(name, ...)
|
|
||||||
local result = self:maybe('on_' .. name, ...)
|
|
||||||
request_render()
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Briefly flashes the element for `options.flash_duration` milliseconds.
|
|
||||||
-- Useful to visualize changes of volume and timeline when changed via hotkeys.
|
|
||||||
function Element:flash()
|
|
||||||
if self.enabled and options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
|
|
||||||
self:tween_stop()
|
|
||||||
self.forced_visibility = 1
|
|
||||||
request_render()
|
|
||||||
self._flash_out_timer:kill()
|
|
||||||
self._flash_out_timer:resume()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Element
|
|
|
@ -1,125 +0,0 @@
|
||||||
local Elements = {itable = {}}
|
|
||||||
|
|
||||||
---@param element Element
|
|
||||||
function Elements:add(element)
|
|
||||||
if not element.id then
|
|
||||||
msg.error('attempt to add element without "id" property')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if self:has(element.id) then Elements:remove(element.id) end
|
|
||||||
|
|
||||||
self.itable[#self.itable + 1] = element
|
|
||||||
self[element.id] = element
|
|
||||||
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Elements:remove(idOrElement)
|
|
||||||
if not idOrElement then return end
|
|
||||||
local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement
|
|
||||||
local element = Elements[id]
|
|
||||||
if element then
|
|
||||||
if not element.destroyed then element:destroy() end
|
|
||||||
element.enabled = false
|
|
||||||
self.itable = itable_remove(self.itable, self[id])
|
|
||||||
self[id] = nil
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Elements:update_proximities()
|
|
||||||
local menu_only = Elements.menu ~= nil
|
|
||||||
local mouse_leave_elements = {}
|
|
||||||
local mouse_enter_elements = {}
|
|
||||||
|
|
||||||
-- Calculates proximities and opacities for defined elements
|
|
||||||
for _, element in self:ipairs() do
|
|
||||||
if element.enabled then
|
|
||||||
local previous_proximity_raw = element.proximity_raw
|
|
||||||
|
|
||||||
-- If menu is open, all other elements have to be disabled
|
|
||||||
if menu_only then
|
|
||||||
if element.ignores_menu then element:update_proximity()
|
|
||||||
else element:reset_proximity() end
|
|
||||||
else
|
|
||||||
element:update_proximity()
|
|
||||||
end
|
|
||||||
|
|
||||||
if element.proximity_raw == 0 then
|
|
||||||
-- Mouse entered element area
|
|
||||||
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
|
|
||||||
mouse_leave_elements[#mouse_leave_elements + 1] = element
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Trigger `mouse_leave` and `mouse_enter` events
|
|
||||||
for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end
|
|
||||||
for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Toggles passed elements' min visibilities between 0 and 1.
|
|
||||||
---@param ids string[] IDs of elements to peek.
|
|
||||||
function Elements:toggle(ids)
|
|
||||||
local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id]:get_visibility() ~= 1 end)
|
|
||||||
self:set_min_visibility(has_invisible and 1 or 0, ids)
|
|
||||||
-- Reset proximities when toggling off. Has to happen after `set_min_visibility`,
|
|
||||||
-- as that is using proximity as a tween starting point.
|
|
||||||
if not has_invisible then
|
|
||||||
for _, id in ipairs(ids) do
|
|
||||||
if Elements[id] then Elements[id]:reset_proximity() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Set (animate) elements' min visibilities to passed value.
|
|
||||||
---@param visibility number 0-1 floating point.
|
|
||||||
---@param ids string[] IDs of elements to peek.
|
|
||||||
function Elements:set_min_visibility(visibility, ids)
|
|
||||||
for _, id in ipairs(ids) do
|
|
||||||
local element = Elements[id]
|
|
||||||
if element then
|
|
||||||
local from = math.max(0, element:get_visibility())
|
|
||||||
element:tween_property('min_visibility', from, visibility)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Flash passed elements.
|
|
||||||
---@param ids string[] IDs of elements to peek.
|
|
||||||
function Elements:flash(ids)
|
|
||||||
local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end)
|
|
||||||
for _, element in ipairs(elements) do element:flash() end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param name string Event name.
|
|
||||||
function Elements:trigger(name, ...)
|
|
||||||
for _, element in self:ipairs() do element:trigger(name, ...) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Trigger two events, `name` and `global_name`, depending on element-cursor proximity.
|
|
||||||
-- Disabled elements don't receive these events.
|
|
||||||
---@param name string Event name.
|
|
||||||
function Elements:proximity_trigger(name, ...)
|
|
||||||
for i = #self.itable, 1, -1 do
|
|
||||||
local element = self.itable[i]
|
|
||||||
if element.enabled 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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Elements:has(id) return self[id] ~= nil end
|
|
||||||
function Elements:ipairs() return ipairs(self.itable) end
|
|
||||||
|
|
||||||
return Elements
|
|
|
@ -1,854 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
-- Menu data structure accepted by `Menu:open(menu)`.
|
|
||||||
---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;}
|
|
||||||
---@alias MenuDataItem MenuDataValue|MenuData
|
|
||||||
---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;}
|
|
||||||
---@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun(); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}
|
|
||||||
|
|
||||||
-- Internal data structure created from `Menu`.
|
|
||||||
---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; submenu_path: integer[]; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling}
|
|
||||||
---@alias MenuStackItem MenuStackValue|MenuStack
|
|
||||||
---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number}
|
|
||||||
---@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean}
|
|
||||||
|
|
||||||
---@alias Modifiers {shift?: boolean, ctrl?: boolean, alt?: boolean}
|
|
||||||
---@alias MenuCallbackMeta {modifiers: Modifiers}
|
|
||||||
---@alias MenuCallback fun(value: any, meta: MenuCallbackMeta)
|
|
||||||
|
|
||||||
---@class Menu : Element
|
|
||||||
local Menu = class(Element)
|
|
||||||
|
|
||||||
---@param data MenuData
|
|
||||||
---@param callback MenuCallback
|
|
||||||
---@param opts? MenuOptions
|
|
||||||
function Menu:open(data, callback, opts)
|
|
||||||
local open_menu = self:is_open()
|
|
||||||
if open_menu then
|
|
||||||
open_menu.is_being_replaced = true
|
|
||||||
open_menu:close(true)
|
|
||||||
end
|
|
||||||
return Menu:new(data, callback, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param menu_type? string
|
|
||||||
---@return Menu|nil
|
|
||||||
function Menu:is_open(menu_type)
|
|
||||||
return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param immediate? boolean Close immediately without fadeout animation.
|
|
||||||
---@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed.
|
|
||||||
---@overload fun(callback: fun())
|
|
||||||
function Menu:close(immediate, callback)
|
|
||||||
if type(immediate) ~= 'boolean' then callback = immediate end
|
|
||||||
|
|
||||||
local menu = self == Menu and Elements.menu or self
|
|
||||||
|
|
||||||
if menu and not menu.destroyed then
|
|
||||||
if menu.is_closing then
|
|
||||||
menu:tween_stop()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local function close()
|
|
||||||
Elements:remove('menu')
|
|
||||||
menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {}
|
|
||||||
menu:disable_key_bindings()
|
|
||||||
Elements:update_proximities()
|
|
||||||
cursor.queue_autohide()
|
|
||||||
if callback then callback() end
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
menu.is_closing = true
|
|
||||||
|
|
||||||
if immediate then close()
|
|
||||||
else menu:fadeout(close) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param data MenuData
|
|
||||||
---@param callback MenuCallback
|
|
||||||
---@param opts? MenuOptions
|
|
||||||
---@return Menu
|
|
||||||
function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end
|
|
||||||
---@param data MenuData
|
|
||||||
---@param callback MenuCallback
|
|
||||||
---@param opts? MenuOptions
|
|
||||||
function Menu:init(data, callback, opts)
|
|
||||||
Element.init(self, 'menu', {ignores_menu = true})
|
|
||||||
|
|
||||||
-----@type fun()
|
|
||||||
self.callback = callback
|
|
||||||
self.opts = opts or {}
|
|
||||||
self.offset_x = 0 -- Used for submenu transition animation.
|
|
||||||
self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items
|
|
||||||
---@type Modifiers|nil
|
|
||||||
self.modifiers = nil
|
|
||||||
self.item_height = nil
|
|
||||||
self.item_spacing = 1
|
|
||||||
self.item_padding = nil
|
|
||||||
self.font_size = nil
|
|
||||||
self.font_size_hint = nil
|
|
||||||
self.scroll_step = nil -- Item height + item spacing.
|
|
||||||
self.scroll_height = nil -- Items + spacings - container height.
|
|
||||||
self.opacity = 0 -- Used to fade in/out.
|
|
||||||
self.type = data.type
|
|
||||||
---@type MenuStack Root MenuStack.
|
|
||||||
self.root = nil
|
|
||||||
---@type MenuStack Current MenuStack.
|
|
||||||
self.current = nil
|
|
||||||
---@type MenuStack[] All menus in a flat array.
|
|
||||||
self.all = nil
|
|
||||||
---@type table<string, MenuStack> Map of submenus by their ids, such as `'Tools > Aspect ratio'`.
|
|
||||||
self.by_id = {}
|
|
||||||
self.key_bindings = {}
|
|
||||||
self.is_being_replaced = false
|
|
||||||
self.is_closing, self.is_closed = false, false
|
|
||||||
---@type {y: integer, time: number}[]
|
|
||||||
self.drag_data = nil
|
|
||||||
self.is_dragging = false
|
|
||||||
|
|
||||||
self:update(data)
|
|
||||||
|
|
||||||
if self.mouse_nav then
|
|
||||||
if self.current then self.current.selected_index = nil end
|
|
||||||
else
|
|
||||||
for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end
|
|
||||||
end
|
|
||||||
|
|
||||||
self:tween_property('opacity', 0, 1)
|
|
||||||
self:enable_key_bindings()
|
|
||||||
Elements.curtain:register('menu')
|
|
||||||
if self.opts.on_open then self.opts.on_open() end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:destroy()
|
|
||||||
Element.destroy(self)
|
|
||||||
self:disable_key_bindings()
|
|
||||||
self.is_closed = true
|
|
||||||
if not self.is_being_replaced then Elements.curtain:unregister('menu') end
|
|
||||||
if self.opts.on_close then self.opts.on_close() end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param data MenuData
|
|
||||||
function Menu:update(data)
|
|
||||||
self.type = data.type
|
|
||||||
|
|
||||||
local new_root = {is_root = true, submenu_path = {}}
|
|
||||||
local new_all = {}
|
|
||||||
local new_by_id = {}
|
|
||||||
local menus_to_serialize = {{new_root, data}}
|
|
||||||
local old_current_id = self.current and self.current.id
|
|
||||||
|
|
||||||
table_assign(new_root, data, {'type', 'title', 'hint', 'keep_open'})
|
|
||||||
|
|
||||||
local i = 0
|
|
||||||
while i < #menus_to_serialize do
|
|
||||||
i = i + 1
|
|
||||||
local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2]
|
|
||||||
local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id
|
|
||||||
if not menu.is_root then
|
|
||||||
menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i)
|
|
||||||
end
|
|
||||||
menu.icon = 'chevron_right'
|
|
||||||
|
|
||||||
-- Update items
|
|
||||||
local first_active_index = nil
|
|
||||||
menu.items = {}
|
|
||||||
|
|
||||||
for i, item_data in ipairs(menu_data.items or {}) do
|
|
||||||
if item_data.active and not first_active_index then first_active_index = i end
|
|
||||||
|
|
||||||
local item = {}
|
|
||||||
table_assign(item, item_data, {
|
|
||||||
'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator',
|
|
||||||
})
|
|
||||||
if item.keep_open == nil then item.keep_open = menu.keep_open end
|
|
||||||
|
|
||||||
-- Submenu
|
|
||||||
if item_data.items then
|
|
||||||
item.parent_menu = menu
|
|
||||||
item.submenu_path = itable_join(menu.submenu_path, {i})
|
|
||||||
menus_to_serialize[#menus_to_serialize + 1] = {item, item_data}
|
|
||||||
end
|
|
||||||
|
|
||||||
menu.items[i] = item
|
|
||||||
end
|
|
||||||
|
|
||||||
if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end
|
|
||||||
|
|
||||||
-- Retain old state
|
|
||||||
local old_menu = self.by_id[menu.is_root and '__root__' or menu.id]
|
|
||||||
if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end
|
|
||||||
|
|
||||||
new_all[#new_all + 1] = menu
|
|
||||||
new_by_id[menu.is_root and '__root__' or menu.id] = menu
|
|
||||||
end
|
|
||||||
|
|
||||||
self.root, self.all, self.by_id = new_root, new_all, new_by_id
|
|
||||||
self.current = self.by_id[old_current_id] or self.root
|
|
||||||
|
|
||||||
self:update_content_dimensions()
|
|
||||||
self:reset_navigation()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param items MenuDataItem[]
|
|
||||||
function Menu:update_items(items)
|
|
||||||
local data = table_shallow_copy(self.root)
|
|
||||||
data.items = items
|
|
||||||
self:update(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:update_content_dimensions()
|
|
||||||
self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height
|
|
||||||
self.font_size = round(self.item_height * 0.48 * options.font_scale)
|
|
||||||
self.font_size_hint = self.font_size - 1
|
|
||||||
self.item_padding = round((self.item_height - self.font_size) * 0.6)
|
|
||||||
self.scroll_step = self.item_height + self.item_spacing
|
|
||||||
|
|
||||||
local title_opts = {size = self.font_size, italic = false, bold = false}
|
|
||||||
local hint_opts = {size = self.font_size_hint}
|
|
||||||
|
|
||||||
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.item_padding
|
|
||||||
|
|
||||||
-- Estimate width of a widest item
|
|
||||||
for _, item in ipairs(menu.items) do
|
|
||||||
local icon_width = item.icon and self.font_size or 0
|
|
||||||
item.title_width = text_width(item.title, title_opts)
|
|
||||||
item.hint_width = text_width(item.hint, hint_opts)
|
|
||||||
local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0)
|
|
||||||
+ (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0)
|
|
||||||
local estimated_width = item.title_width + item.hint_width + icon_width
|
|
||||||
+ (self.item_padding * spacings_in_item)
|
|
||||||
if estimated_width > max_width then max_width = estimated_width end
|
|
||||||
end
|
|
||||||
|
|
||||||
menu.max_width = max_width
|
|
||||||
end
|
|
||||||
|
|
||||||
self:update_dimensions()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:update_dimensions()
|
|
||||||
-- Coordinates and sizes are of the scrollable area to make
|
|
||||||
-- consuming values in rendering and collisions easier. Title is rendered
|
|
||||||
-- above it, so we need to account for that in max_height and ay position.
|
|
||||||
local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width
|
|
||||||
|
|
||||||
for _, menu in ipairs(self.all) do
|
|
||||||
menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9))
|
|
||||||
local title_height = (menu.is_root and menu.title) and self.scroll_step or 0
|
|
||||||
local max_height = round((display.height - title_height) * 0.9)
|
|
||||||
local content_height = self.scroll_step * #menu.items
|
|
||||||
menu.height = math.min(content_height - self.item_spacing, max_height)
|
|
||||||
menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5))
|
|
||||||
menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0)
|
|
||||||
menu.scroll_y = menu.scroll_y or 0
|
|
||||||
self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits
|
|
||||||
end
|
|
||||||
|
|
||||||
self:update_coordinates()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Updates element coordinates to match 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)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:reset_navigation()
|
|
||||||
local menu = self.current
|
|
||||||
|
|
||||||
-- Reset indexes and scroll
|
|
||||||
self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits
|
|
||||||
if self.mouse_nav then
|
|
||||||
self:select_item_below_cursor()
|
|
||||||
else
|
|
||||||
self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Walk up the parent menu chain and activate items that lead to current menu
|
|
||||||
local parent = menu.parent_menu
|
|
||||||
while parent do
|
|
||||||
parent.selected_index = itable_index_of(parent.items, menu)
|
|
||||||
menu, parent = parent, parent.parent_menu
|
|
||||||
end
|
|
||||||
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:set_offset_x(offset)
|
|
||||||
local delta = offset - self.offset_x
|
|
||||||
self.offset_x = offset
|
|
||||||
self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end
|
|
||||||
|
|
||||||
function Menu:get_item_index_below_cursor()
|
|
||||||
local menu = self.current
|
|
||||||
if #menu.items < 1 or self.proximity_raw > 0 then return nil end
|
|
||||||
return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:get_first_active_index(menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
for index, item in ipairs(self.current.items) do
|
|
||||||
if item.active then return index end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param pos? number
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:set_scroll_to(pos, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
menu.scroll_y = clamp(0, pos or 0, menu.scroll_height)
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param delta? number
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:set_scroll_by(delta, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
self:set_scroll_to(menu.scroll_y + delta, menu)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param pos? number
|
|
||||||
---@param menu? MenuStack
|
|
||||||
---@param fling_options? table
|
|
||||||
function Menu:scroll_to(pos, menu, fling_options)
|
|
||||||
menu = menu or self.current
|
|
||||||
menu.fling = {
|
|
||||||
y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y),
|
|
||||||
time = mp.get_time(), duration = 0.1, easing = ease_out_sext,
|
|
||||||
}
|
|
||||||
if fling_options then table_assign(menu.fling, fling_options) end
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param delta? number
|
|
||||||
---@param menu? MenuStack
|
|
||||||
---@param fling_options? Fling
|
|
||||||
function Menu:scroll_by(delta, menu, fling_options)
|
|
||||||
menu = menu or self.current
|
|
||||||
self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param index? integer
|
|
||||||
---@param menu? MenuStack
|
|
||||||
---@param immediate? boolean
|
|
||||||
function Menu:scroll_to_index(index, menu, immediate)
|
|
||||||
menu = menu or self.current
|
|
||||||
if (index and index >= 1 and index <= #menu.items) then
|
|
||||||
local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2))
|
|
||||||
if immediate then self:set_scroll_to(position, menu)
|
|
||||||
else self:scroll_to(position, menu) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param index? integer
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:select_index(index, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param value? any
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:select_value(value, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
local index = itable_find(menu.items, function(item) return item.value == value end)
|
|
||||||
self:select_index(index)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:deactivate_items(menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
for _, item in ipairs(menu.items) do item.active = false end
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param index? integer
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:activate_index(index, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param index? integer
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:activate_one_index(index, menu)
|
|
||||||
self:deactivate_items(menu)
|
|
||||||
self:activate_index(index, menu)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param value? any
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:activate_value(value, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
local index = itable_find(menu.items, function(item) return item.value == value end)
|
|
||||||
self:activate_index(index, menu)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param value? any
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:activate_one_value(value, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
local index = itable_find(menu.items, function(item) return item.value == value end)
|
|
||||||
self:activate_one_index(index, menu)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param menu MenuStack One of menus in `self.all`.
|
|
||||||
function Menu:activate_menu(menu)
|
|
||||||
if itable_index_of(self.all, menu) then
|
|
||||||
self.current = menu
|
|
||||||
self:update_coordinates()
|
|
||||||
self:reset_navigation()
|
|
||||||
request_render()
|
|
||||||
else
|
|
||||||
msg.error('Attempt to open a menu not in `self.all` list.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
function Menu:activate_submenu(id)
|
|
||||||
local submenu = self.by_id[id]
|
|
||||||
if submenu then self:activate_menu(submenu)
|
|
||||||
else msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param index? integer
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:delete_index(index, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
if (index and index >= 1 and index <= #menu.items) then
|
|
||||||
table.remove(menu.items, index)
|
|
||||||
self:update_content_dimensions()
|
|
||||||
self:scroll_to_index(menu.selected_index, menu)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param value? any
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:delete_value(value, menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
local index = itable_find(menu.items, function(item) return item.value == value end)
|
|
||||||
self:delete_index(index)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:prev(menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1)
|
|
||||||
self:scroll_to_index(menu.selected_index, menu, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param menu? MenuStack
|
|
||||||
function Menu:next(menu)
|
|
||||||
menu = menu or self.current
|
|
||||||
menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items)
|
|
||||||
self:scroll_to_index(menu.selected_index, menu, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:back()
|
|
||||||
if self.opts.on_back then
|
|
||||||
self.opts.on_back()
|
|
||||||
if self.is_closed then return end
|
|
||||||
end
|
|
||||||
|
|
||||||
local menu = self.current
|
|
||||||
local parent = menu.parent_menu
|
|
||||||
|
|
||||||
if parent then
|
|
||||||
menu.selected_index = nil
|
|
||||||
self:activate_menu(parent)
|
|
||||||
self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end)
|
|
||||||
self.opacity = 1 -- in case tween above canceled fade in animation
|
|
||||||
else
|
|
||||||
self:close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean}
|
|
||||||
function Menu:open_selected_item(opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local menu = self.current
|
|
||||||
if menu.selected_index then
|
|
||||||
local item = menu.items[menu.selected_index]
|
|
||||||
-- Is submenu
|
|
||||||
if item.items then
|
|
||||||
if opts.preselect_submenu_item then
|
|
||||||
item.selected_index = #item.items > 0 and 1 or nil
|
|
||||||
end
|
|
||||||
self:activate_menu(item)
|
|
||||||
self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end)
|
|
||||||
self.opacity = 1 -- in case tween above canceled fade in animation
|
|
||||||
else
|
|
||||||
self.callback(item.value, {modifiers = self.modifiers or {}})
|
|
||||||
if not item.keep_open and not opts.keep_open then self:close() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end
|
|
||||||
function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end
|
|
||||||
function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end
|
|
||||||
|
|
||||||
---@param index integer
|
|
||||||
function Menu:move_selected_item_to(index)
|
|
||||||
local from, callback = self.current.selected_index, self.opts.on_move_item
|
|
||||||
if callback and from and from ~= index and index >= 1 and index <= #self.current.items then
|
|
||||||
callback(from, index, self.current.submenu_path)
|
|
||||||
self.current.selected_index = index
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:move_selected_item_up()
|
|
||||||
if self.current.selected_index then self:move_selected_item_to(self.current.selected_index - 1) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:move_selected_item_down()
|
|
||||||
if self.current.selected_index then self:move_selected_item_to(self.current.selected_index + 1) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:delete_selected_item()
|
|
||||||
local index, callback = self.current.selected_index, self.opts.on_delete_item
|
|
||||||
if callback and index then callback(index, self.current.submenu_path) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:on_display() self:update_dimensions() end
|
|
||||||
function Menu:on_prop_fullormaxed() self:update_content_dimensions() end
|
|
||||||
|
|
||||||
function Menu:handle_cursor_down()
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
self.drag_data = {{y = cursor.y, time = mp.get_time()}}
|
|
||||||
self.current.fling = nil
|
|
||||||
else
|
|
||||||
if cursor.x < self.ax and self.current.parent_menu then self:back()
|
|
||||||
else self:close() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:fling_distance()
|
|
||||||
local first, last = self.drag_data[1], self.drag_data[#self.drag_data]
|
|
||||||
if mp.get_time() - last.time > 0.05 then return 0 end
|
|
||||||
for i = #self.drag_data - 1, 1, -1 do
|
|
||||||
local drag = self.drag_data[i]
|
|
||||||
if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end
|
|
||||||
end
|
|
||||||
return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:handle_cursor_up()
|
|
||||||
if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then
|
|
||||||
self:select_item_below_cursor()
|
|
||||||
self:open_selected_item({preselect_submenu_item = false, keep_open = self.modifiers and self.modifiers.shift})
|
|
||||||
end
|
|
||||||
if self.is_dragging then
|
|
||||||
local distance = self:fling_distance()
|
|
||||||
if math.abs(distance) > 50 then
|
|
||||||
self.current.fling = {
|
|
||||||
y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time,
|
|
||||||
easing = ease_out_quart, duration = 0.5, update_cursor = true,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.is_dragging = false
|
|
||||||
self.drag_data = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function Menu:on_global_mouse_move()
|
|
||||||
self.mouse_nav = true
|
|
||||||
if self.drag_data then
|
|
||||||
self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10
|
|
||||||
local distance = self.drag_data[#self.drag_data].y - cursor.y
|
|
||||||
if distance ~= 0 then self:set_scroll_by(distance) end
|
|
||||||
self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()}
|
|
||||||
end
|
|
||||||
if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor()
|
|
||||||
else self.current.selected_index = nil end
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:handle_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end
|
|
||||||
function Menu:handle_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end
|
|
||||||
|
|
||||||
function Menu:on_pgup()
|
|
||||||
local menu = self.current
|
|
||||||
local items_per_page = round((menu.height / self.scroll_step) * 0.4)
|
|
||||||
local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page
|
|
||||||
menu.selected_index = clamp(1, paged_index, #menu.items)
|
|
||||||
if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:on_pgdwn()
|
|
||||||
local menu = self.current
|
|
||||||
local items_per_page = round((menu.height / self.scroll_step) * 0.4)
|
|
||||||
local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page
|
|
||||||
menu.selected_index = clamp(1, paged_index, #menu.items)
|
|
||||||
if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:on_home()
|
|
||||||
self.current.selected_index = math.min(1, #self.current.items)
|
|
||||||
if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:on_end()
|
|
||||||
self.current.selected_index = #self.current.items
|
|
||||||
if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:add_key_binding(key, name, fn, flags)
|
|
||||||
self.key_bindings[#self.key_bindings + 1] = name
|
|
||||||
mp.add_forced_key_binding(key, name, fn, flags)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:enable_key_bindings()
|
|
||||||
-- The `mp.set_key_bindings()` method would be easier here, but that
|
|
||||||
-- doesn't support 'repeatable' flag, so we are stuck with this monster.
|
|
||||||
self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable')
|
|
||||||
self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable')
|
|
||||||
self:add_key_binding('ctrl+up', 'menu-move-up', self:create_key_action('move_selected_item_up'), 'repeatable')
|
|
||||||
self:add_key_binding('ctrl+down', 'menu-move-down', self:create_key_action('move_selected_item_down'), 'repeatable')
|
|
||||||
self:add_key_binding('left', 'menu-back1', self:create_key_action('back'))
|
|
||||||
self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect'))
|
|
||||||
self:add_key_binding('shift+right', 'menu-select-soft1',
|
|
||||||
self:create_key_action('open_selected_item_soft', {shift = true}))
|
|
||||||
self:add_key_binding('shift+mbtn_left', 'menu-select3', self:create_modified_mbtn_left_handler({shift = true}))
|
|
||||||
self:add_key_binding('ctrl+mbtn_left', 'menu-select4', self:create_modified_mbtn_left_handler({ctrl = true}))
|
|
||||||
self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back'))
|
|
||||||
self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back'))
|
|
||||||
self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect'))
|
|
||||||
self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect'))
|
|
||||||
self:add_key_binding('ctrl+enter', 'menu-select-ctrl1',
|
|
||||||
self:create_key_action('open_selected_item_preselect', {ctrl = true}))
|
|
||||||
self:add_key_binding('ctrl+kp_enter', 'menu-select-ctrl2',
|
|
||||||
self:create_key_action('open_selected_item_preselect', {ctrl = true}))
|
|
||||||
self:add_key_binding('shift+enter', 'menu-select-alt5',
|
|
||||||
self:create_key_action('open_selected_item_soft', {shift = true}))
|
|
||||||
self:add_key_binding('shift+kp_enter', 'menu-select-alt6',
|
|
||||||
self:create_key_action('open_selected_item_soft', {shift = true}))
|
|
||||||
self:add_key_binding('esc', 'menu-close', self:create_key_action('close'))
|
|
||||||
self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable')
|
|
||||||
self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable')
|
|
||||||
self:add_key_binding('home', 'menu-home', self:create_key_action('on_home'))
|
|
||||||
self:add_key_binding('end', 'menu-end', self:create_key_action('on_end'))
|
|
||||||
self:add_key_binding('del', 'menu-delete-item', self:create_key_action('delete_selected_item'))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:disable_key_bindings()
|
|
||||||
for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end
|
|
||||||
self.key_bindings = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param modifiers Modifiers
|
|
||||||
function Menu:create_modified_mbtn_left_handler(modifiers)
|
|
||||||
return function()
|
|
||||||
self.mouse_nav = true
|
|
||||||
self.modifiers = modifiers
|
|
||||||
self:handle_cursor_down()
|
|
||||||
self:handle_cursor_up()
|
|
||||||
self.modifiers = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param modifiers? Modifiers
|
|
||||||
function Menu:create_key_action(name, modifiers)
|
|
||||||
return function()
|
|
||||||
self.mouse_nav = false
|
|
||||||
self.modifiers = modifiers
|
|
||||||
self:maybe(name)
|
|
||||||
self.modifiers = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Menu:render()
|
|
||||||
local update_cursor = false
|
|
||||||
for _, menu in ipairs(self.all) do
|
|
||||||
if menu.fling then
|
|
||||||
update_cursor = update_cursor or menu.fling.update_cursor or false
|
|
||||||
local time_delta = state.render_last_time - menu.fling.time
|
|
||||||
local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1))
|
|
||||||
self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu)
|
|
||||||
if progress < 1 then request_render() else menu.fling = nil end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if update_cursor then self:select_item_below_cursor() end
|
|
||||||
|
|
||||||
cursor.on_primary_down = function() self:handle_cursor_down() end
|
|
||||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
cursor.on_wheel_down = function() self:handle_wheel_down() end
|
|
||||||
cursor.on_wheel_up = function() self:handle_wheel_up() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
local opacity = options.menu_opacity * self.opacity
|
|
||||||
local spacing = self.item_padding
|
|
||||||
local icon_size = self.font_size
|
|
||||||
|
|
||||||
function draw_menu(menu, x, y, opacity)
|
|
||||||
local ax, ay, bx, by = x, y, x + menu.width, y + menu.height
|
|
||||||
local draw_title = menu.is_root and menu.title
|
|
||||||
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. 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 selected_index = menu.selected_index or -1
|
|
||||||
-- remove menu_opacity to start off with full opacity, but still decay for parent menus
|
|
||||||
local text_opacity = opacity / options.menu_opacity
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, {
|
|
||||||
color = bg, opacity = opacity, radius = 4,
|
|
||||||
})
|
|
||||||
|
|
||||||
for index = start_index, end_index, 1 do
|
|
||||||
local item = menu.items[index]
|
|
||||||
local next_item = menu.items[index + 1]
|
|
||||||
local is_highlighted = selected_index == index or item.active
|
|
||||||
local next_is_active = next_item and next_item.active
|
|
||||||
local next_is_highlighted = selected_index == index + 1 or next_is_active
|
|
||||||
|
|
||||||
if not item then break end
|
|
||||||
|
|
||||||
local item_ay = 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 + spacing, bx - spacing
|
|
||||||
local font_color = item.active and fgt or bgt
|
|
||||||
local shadow_color = item.active and fg or bg
|
|
||||||
|
|
||||||
-- Separator
|
|
||||||
local separator_ay = item.separator and item_by - 1 or item_by
|
|
||||||
local separator_by = item_by + (item.separator and 2 or 1)
|
|
||||||
if is_highlighted then separator_ay = item_by + 1 end
|
|
||||||
if next_is_highlighted then separator_by = item_by end
|
|
||||||
if separator_by - separator_ay > 0 and item_by < by then
|
|
||||||
ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, {
|
|
||||||
color = fg, opacity = opacity * (item.separator and 0.08 or 0.06),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Highlight
|
|
||||||
local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0)
|
|
||||||
if highlight_opacity > 0 then
|
|
||||||
ass:rect(ax + 2, item_ay, bx - 2, item_by, {
|
|
||||||
radius = 2, color = fg, opacity = highlight_opacity * text_opacity,
|
|
||||||
clip = item_clip,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Icon
|
|
||||||
if item.icon then
|
|
||||||
local x, y = content_bx - (icon_size / 2), item_center_y
|
|
||||||
if item.icon == 'spinner' then
|
|
||||||
ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8})
|
|
||||||
else
|
|
||||||
ass:icon(x, y, icon_size * 1.5, item.icon, {
|
|
||||||
color = font_color, opacity = text_opacity, clip = item_clip,
|
|
||||||
shadow = 1, shadow_color = shadow_color,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
content_bx = content_bx - icon_size - spacing
|
|
||||||
end
|
|
||||||
|
|
||||||
local title_cut_x = content_bx
|
|
||||||
if item.hint_width > 0 then
|
|
||||||
-- controls title & hint clipping proportional to the ratio of their widths
|
|
||||||
local title_content_ratio = item.title_width / (item.title_width + item.hint_width)
|
|
||||||
title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio
|
|
||||||
+ (item.title_width > 0 and spacing / 2 or 0))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hint
|
|
||||||
if item.hint then
|
|
||||||
item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint)
|
|
||||||
local clip = '\\clip(' .. title_cut_x .. ',' ..
|
|
||||||
math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')'
|
|
||||||
ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, {
|
|
||||||
size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip,
|
|
||||||
shadow = 1, shadow_color = shadow_color,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 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_cut_x .. ',' .. math.min(item_by, by) .. ')'
|
|
||||||
ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, {
|
|
||||||
size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2,
|
|
||||||
opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip,
|
|
||||||
shadow = 1, shadow_color = shadow_color,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Menu title
|
|
||||||
if draw_title then
|
|
||||||
local title_ay = ay - self.item_height
|
|
||||||
local title_height = self.item_height - 3
|
|
||||||
menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title)
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, {
|
|
||||||
color = fg, opacity = opacity * 0.8, radius = 2,
|
|
||||||
})
|
|
||||||
ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', {
|
|
||||||
size = 80, color = bg, opacity = opacity * 0.1,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Title
|
|
||||||
ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, {
|
|
||||||
size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity,
|
|
||||||
clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')',
|
|
||||||
})
|
|
||||||
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))
|
|
||||||
ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Main menu
|
|
||||||
draw_menu(self.current, self.ax, self.ay, opacity)
|
|
||||||
|
|
||||||
-- Parent menus
|
|
||||||
local parent_menu = self.current.parent_menu
|
|
||||||
local parent_offset_x = self.ax
|
|
||||||
local parent_opacity_factor = options.menu_parent_opacity
|
|
||||||
local menu_gap = 2
|
|
||||||
|
|
||||||
while parent_menu do
|
|
||||||
parent_offset_x = parent_offset_x - parent_menu.width - menu_gap
|
|
||||||
draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity)
|
|
||||||
parent_opacity_factor = parent_opacity_factor * parent_opacity_factor
|
|
||||||
parent_menu = parent_menu.parent_menu
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Selected menu
|
|
||||||
local selected_menu = self.current.items[self.current.selected_index]
|
|
||||||
|
|
||||||
if selected_menu and selected_menu.items then
|
|
||||||
draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Menu
|
|
|
@ -1,80 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@class PauseIndicator : Element
|
|
||||||
local PauseIndicator = class(Element)
|
|
||||||
|
|
||||||
function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end
|
|
||||||
function PauseIndicator:init()
|
|
||||||
Element.init(self, 'pause_indicator')
|
|
||||||
self.ignores_menu = true
|
|
||||||
self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8
|
|
||||||
self.paused = state.pause
|
|
||||||
self.type = options.pause_indicator
|
|
||||||
self.is_manual = options.pause_indicator == 'manual'
|
|
||||||
self.fadeout_requested = false
|
|
||||||
self.opacity = 0
|
|
||||||
|
|
||||||
mp.observe_property('pause', 'bool', function(_, paused)
|
|
||||||
if Elements.timeline.pressed then return end
|
|
||||||
if options.pause_indicator == 'flash' then
|
|
||||||
if self.paused == paused then return end
|
|
||||||
self:flash()
|
|
||||||
elseif options.pause_indicator == 'static' then
|
|
||||||
self:decide()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PauseIndicator:flash()
|
|
||||||
if not self.is_manual and self.type ~= 'flash' then return end
|
|
||||||
-- can't wait for pause property event listener to set this, because when this is used inside a binding like:
|
|
||||||
-- cycle pause; script-binding uosc/flash-pause-indicator
|
|
||||||
-- the pause event is not fired fast enough, and indicator starts rendering with old icon
|
|
||||||
self.paused = mp.get_property_native('pause')
|
|
||||||
if self.is_manual then self.type = 'flash' end
|
|
||||||
self.opacity = 1
|
|
||||||
self:tween_property('opacity', 1, 0, 0.15)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- decides whether static indicator should be visible or not
|
|
||||||
function PauseIndicator:decide()
|
|
||||||
if not self.is_manual and self.type ~= 'static' then return end
|
|
||||||
self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary
|
|
||||||
if self.is_manual then self.type = 'static' end
|
|
||||||
self.opacity = self.paused and 1 or 0
|
|
||||||
request_render()
|
|
||||||
|
|
||||||
-- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored.
|
|
||||||
-- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more
|
|
||||||
mp.add_timeout(.05, function() osd:update() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PauseIndicator:render()
|
|
||||||
if self.opacity == 0 then return end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
local is_static = self.type == 'static'
|
|
||||||
|
|
||||||
-- Background fadeout
|
|
||||||
if is_static then
|
|
||||||
ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Icon
|
|
||||||
local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15))
|
|
||||||
size = size + size * (1 - self.opacity)
|
|
||||||
|
|
||||||
if self.paused then
|
|
||||||
ass:icon(display.width / 2, display.height / 2, size, 'pause',
|
|
||||||
{border = 1, opacity = self.base_icon_opacity * self.opacity}
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow',
|
|
||||||
{border = 1, opacity = self.base_icon_opacity * self.opacity}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return PauseIndicator
|
|
|
@ -1,192 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; }
|
|
||||||
|
|
||||||
---@class Speed : Element
|
|
||||||
local Speed = class(Element)
|
|
||||||
|
|
||||||
---@param props? ElementProps
|
|
||||||
function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end
|
|
||||||
function Speed:init(props)
|
|
||||||
Element.init(self, 'speed', props)
|
|
||||||
|
|
||||||
self.width = 0
|
|
||||||
self.height = 0
|
|
||||||
self.notches = 10
|
|
||||||
self.notch_every = 0.1
|
|
||||||
---@type number
|
|
||||||
self.notch_spacing = nil
|
|
||||||
---@type number
|
|
||||||
self.font_size = nil
|
|
||||||
---@type Dragging|nil
|
|
||||||
self.dragging = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:on_coordinates()
|
|
||||||
self.height, self.width = self.by - self.ay, self.bx - self.ax
|
|
||||||
self.notch_spacing = self.width / (self.notches + 1)
|
|
||||||
self.font_size = round(self.height * 0.48 * options.font_scale)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:speed_step(speed, up)
|
|
||||||
if options.speed_step_is_factor then
|
|
||||||
if up then
|
|
||||||
return speed * options.speed_step
|
|
||||||
else
|
|
||||||
return speed * 1 / options.speed_step
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if up then
|
|
||||||
return speed + options.speed_step
|
|
||||||
else
|
|
||||||
return speed - options.speed_step
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:handle_cursor_down()
|
|
||||||
self:tween_stop() -- Stop and cleanup possible ongoing animations
|
|
||||||
self.dragging = {
|
|
||||||
start_time = mp.get_time(),
|
|
||||||
start_x = cursor.x,
|
|
||||||
distance = 0,
|
|
||||||
speed_distance = 0,
|
|
||||||
start_speed = state.speed,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:on_global_mouse_move()
|
|
||||||
if not self.dragging then return end
|
|
||||||
|
|
||||||
self.dragging.distance = cursor.x - self.dragging.start_x
|
|
||||||
self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every)
|
|
||||||
|
|
||||||
local speed_current = state.speed
|
|
||||||
local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance
|
|
||||||
speed_drag_current = clamp(0.01, speed_drag_current, 100)
|
|
||||||
local drag_dir_up = speed_drag_current > speed_current
|
|
||||||
|
|
||||||
local speed_step_next = speed_current
|
|
||||||
local speed_drag_diff = math.abs(speed_drag_current - speed_current)
|
|
||||||
while math.abs(speed_step_next - speed_current) < speed_drag_diff do
|
|
||||||
speed_step_next = self:speed_step(speed_step_next, drag_dir_up)
|
|
||||||
end
|
|
||||||
local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up)
|
|
||||||
|
|
||||||
local speed_new = speed_step_prev
|
|
||||||
local speed_next_diff = math.abs(speed_drag_current - speed_step_next)
|
|
||||||
local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev)
|
|
||||||
if speed_next_diff < speed_prev_diff then
|
|
||||||
speed_new = speed_step_next
|
|
||||||
end
|
|
||||||
|
|
||||||
if speed_new ~= speed_current then
|
|
||||||
mp.set_property_native('speed', speed_new)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:handle_cursor_up()
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
-- Reset speed on short clicks
|
|
||||||
if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then
|
|
||||||
mp.set_property_native('speed', 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.dragging = nil
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:on_global_mouse_leave()
|
|
||||||
self.dragging = nil
|
|
||||||
request_render()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Speed:handle_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end
|
|
||||||
function Speed:handle_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end
|
|
||||||
|
|
||||||
function Speed:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
local opacity = self.dragging and 1 or visibility
|
|
||||||
|
|
||||||
if opacity <= 0 then return end
|
|
||||||
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
cursor.on_primary_down = function()
|
|
||||||
self:handle_cursor_down()
|
|
||||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
|
||||||
end
|
|
||||||
cursor.on_wheel_down = function() self:handle_wheel_down() end
|
|
||||||
cursor.on_wheel_up = function() self:handle_wheel_up() end
|
|
||||||
end
|
|
||||||
if self.dragging then
|
|
||||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity})
|
|
||||||
|
|
||||||
-- Coordinates
|
|
||||||
local ax, ay = self.ax, self.ay
|
|
||||||
local bx, by = self.bx, ay + self.height
|
|
||||||
local half_width = (self.width / 2)
|
|
||||||
local half_x = ax + half_width
|
|
||||||
|
|
||||||
-- Notches
|
|
||||||
local speed_at_center = state.speed
|
|
||||||
if self.dragging then
|
|
||||||
speed_at_center = self.dragging.start_speed + self.dragging.speed_distance
|
|
||||||
speed_at_center = clamp(0.01, speed_at_center, 100)
|
|
||||||
end
|
|
||||||
local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every
|
|
||||||
local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing)
|
|
||||||
local guide_size = math.floor(self.height / 7.5)
|
|
||||||
local notch_by = by - guide_size
|
|
||||||
local notch_ay_big = ay + round(self.font_size * 1.1)
|
|
||||||
local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
|
|
||||||
local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
|
|
||||||
local from_to_index = math.floor(self.notches / 2)
|
|
||||||
|
|
||||||
for i = -from_to_index, from_to_index do
|
|
||||||
local notch_speed = nearest_notch_speed + (i * self.notch_every)
|
|
||||||
|
|
||||||
if notch_speed >= 0 and notch_speed <= 100 then
|
|
||||||
local notch_x = nearest_notch_x + (i * self.notch_spacing)
|
|
||||||
local notch_thickness = 1
|
|
||||||
local notch_ay = notch_ay_small
|
|
||||||
if (notch_speed % (self.notch_every * 10)) < 0.00000001 then
|
|
||||||
notch_ay = notch_ay_big
|
|
||||||
notch_thickness = 1.5
|
|
||||||
elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then
|
|
||||||
notch_ay = notch_ay_medium
|
|
||||||
end
|
|
||||||
|
|
||||||
ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, {
|
|
||||||
color = fg, border = 1, border_color = bg,
|
|
||||||
opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Center guide
|
|
||||||
ass:new_event()
|
|
||||||
ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}')
|
|
||||||
ass:opacity(opacity)
|
|
||||||
ass:pos(0, 0)
|
|
||||||
ass:draw_start()
|
|
||||||
ass:move_to(half_x, by - 2 - guide_size)
|
|
||||||
ass:line_to(half_x + guide_size, by - 2)
|
|
||||||
ass:line_to(half_x - guide_size, by - 2)
|
|
||||||
ass:draw_stop()
|
|
||||||
|
|
||||||
-- Speed value
|
|
||||||
local speed_text = (round(state.speed * 100) / 100) .. 'x'
|
|
||||||
ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, {
|
|
||||||
size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Speed
|
|
|
@ -1,430 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@class Timeline : Element
|
|
||||||
local Timeline = class(Element)
|
|
||||||
|
|
||||||
function Timeline:new() return Class.new(self) --[[@as Timeline]] end
|
|
||||||
function Timeline:init()
|
|
||||||
Element.init(self, 'timeline')
|
|
||||||
---@type false|{pause: boolean, distance: number, last: {x: number, y: number}}
|
|
||||||
self.pressed = false
|
|
||||||
self.obstructed = false
|
|
||||||
self.size_max = 0
|
|
||||||
self.size_min = 0
|
|
||||||
self.size_min_override = options.timeline_start_hidden and 0 or nil
|
|
||||||
self.font_size = 0
|
|
||||||
self.top_border = options.timeline_border
|
|
||||||
self.is_hovered = false
|
|
||||||
self.has_thumbnail = false
|
|
||||||
|
|
||||||
-- Delayed seeking timer
|
|
||||||
self.seek_timer = mp.add_timeout(0.05, function() self:set_from_cursor() end)
|
|
||||||
self.seek_timer:kill()
|
|
||||||
|
|
||||||
-- Release any dragging when file gets unloaded
|
|
||||||
mp.register_event('end-file', function() self.pressed = false end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_visibility()
|
|
||||||
return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self))
|
|
||||||
or Element.get_visibility(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:decide_enabled()
|
|
||||||
local previous = self.enabled
|
|
||||||
self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil
|
|
||||||
if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_effective_size_min()
|
|
||||||
return self.size_min_override or self.size_min
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_effective_size()
|
|
||||||
if Elements.speed and Elements.speed.dragging then return self.size_max end
|
|
||||||
local size_min = self:get_effective_size_min()
|
|
||||||
return size_min + math.ceil((self.size_max - size_min) * self:get_visibility())
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_effective_line_width()
|
|
||||||
return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_is_hovered() return self.enabled and self.is_hovered end
|
|
||||||
|
|
||||||
function Timeline:update_dimensions()
|
|
||||||
if state.fullormaxed then
|
|
||||||
self.size_min = options.timeline_size_min_fullscreen
|
|
||||||
self.size_max = options.timeline_size_max_fullscreen
|
|
||||||
else
|
|
||||||
self.size_min = options.timeline_size_min
|
|
||||||
self.size_max = options.timeline_size_max
|
|
||||||
end
|
|
||||||
self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale)
|
|
||||||
self.ax = Elements.window_border.size
|
|
||||||
self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border
|
|
||||||
self.bx = display.width - Elements.window_border.size
|
|
||||||
self.by = display.height - Elements.window_border.size
|
|
||||||
self.width = self.bx - self.ax
|
|
||||||
self.chapter_size = math.max((self.by - self.ay) / 10, 3)
|
|
||||||
self.chapter_size_hover = self.chapter_size * 2
|
|
||||||
|
|
||||||
-- Disable if not enough space
|
|
||||||
local available_space = display.height - Elements.window_border.size * 2
|
|
||||||
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
|
|
||||||
self.obstructed = available_space < self.size_max + 10
|
|
||||||
self:decide_enabled()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:get_time_at_x(x)
|
|
||||||
local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0)
|
|
||||||
local time_width = self.width - line_width - 1
|
|
||||||
local fax = (time_width) * state.time / state.duration
|
|
||||||
local fbx = fax + line_width
|
|
||||||
-- time starts 0.5 pixels in
|
|
||||||
x = x - self.ax - 0.5
|
|
||||||
if x > fbx then x = x - line_width
|
|
||||||
elseif x > fax then x = fax end
|
|
||||||
local progress = clamp(0, x / time_width, 1)
|
|
||||||
return state.duration * progress
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param fast? boolean
|
|
||||||
function Timeline:set_from_cursor(fast)
|
|
||||||
if state.time and state.duration then
|
|
||||||
mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:clear_thumbnail()
|
|
||||||
mp.commandv('script-message-to', 'thumbfast', 'clear')
|
|
||||||
self.has_thumbnail = false
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:handle_cursor_down()
|
|
||||||
self.pressed = {pause = state.pause, distance = 0, last = {x = cursor.x, y = cursor.y}}
|
|
||||||
mp.set_property_native('pause', true)
|
|
||||||
self:set_from_cursor()
|
|
||||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
|
||||||
end
|
|
||||||
function Timeline:on_prop_duration() self:decide_enabled() end
|
|
||||||
function Timeline:on_prop_time() self:decide_enabled() end
|
|
||||||
function Timeline:on_prop_border() self:update_dimensions() end
|
|
||||||
function Timeline:on_prop_fullormaxed() self:update_dimensions() end
|
|
||||||
function Timeline:on_display() self:update_dimensions() end
|
|
||||||
function Timeline:handle_cursor_up()
|
|
||||||
self.seek_timer:kill()
|
|
||||||
if self.pressed then
|
|
||||||
mp.set_property_native('pause', self.pressed.pause)
|
|
||||||
self.pressed = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function Timeline:on_global_mouse_leave()
|
|
||||||
self.pressed = false
|
|
||||||
end
|
|
||||||
|
|
||||||
function Timeline:on_global_mouse_move()
|
|
||||||
if self.pressed then
|
|
||||||
self.pressed.distance = self.pressed.distance + get_point_to_point_proximity(self.pressed.last, cursor)
|
|
||||||
self.pressed.last.x, self.pressed.last.y = cursor.x, cursor.y
|
|
||||||
if self.width / state.duration < 10 then
|
|
||||||
self:set_from_cursor(true)
|
|
||||||
self.seek_timer:kill()
|
|
||||||
self.seek_timer:resume()
|
|
||||||
else self:set_from_cursor() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function Timeline:handle_wheel_up() mp.commandv('seek', options.timeline_step) end
|
|
||||||
function Timeline:handle_wheel_down() mp.commandv('seek', -options.timeline_step) end
|
|
||||||
|
|
||||||
function Timeline:render()
|
|
||||||
if self.size_max == 0 then return end
|
|
||||||
|
|
||||||
local size_min = self:get_effective_size_min()
|
|
||||||
local size = self:get_effective_size()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
self.is_hovered = false
|
|
||||||
|
|
||||||
if size < 1 then
|
|
||||||
if self.has_thumbnail then self:clear_thumbnail() end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
self.is_hovered = true
|
|
||||||
cursor.on_primary_down = function() self:handle_cursor_down() end
|
|
||||||
cursor.on_wheel_down = function() self:handle_wheel_down() end
|
|
||||||
cursor.on_wheel_up = function() self:handle_wheel_up() end
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.pressed then
|
|
||||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
|
|
||||||
local hide_text_below = math.max(self.font_size * 0.8, size_min * 2)
|
|
||||||
local hide_text_ramp = hide_text_below / 2
|
|
||||||
local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp
|
|
||||||
|
|
||||||
local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4)
|
|
||||||
local progress = state.time / state.duration
|
|
||||||
local is_line = options.timeline_style == 'line'
|
|
||||||
|
|
||||||
-- Foreground & Background bar coordinates
|
|
||||||
local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by
|
|
||||||
local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby
|
|
||||||
local fcy = fay + (size / 2)
|
|
||||||
|
|
||||||
local line_width = 0
|
|
||||||
|
|
||||||
if is_line then
|
|
||||||
local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1)
|
|
||||||
local line_width_max = self:get_effective_line_width()
|
|
||||||
local max_min_width_delta = size_min > 0
|
|
||||||
and line_width_max - line_width_max * options.timeline_line_width_minimized_scale
|
|
||||||
or 0
|
|
||||||
line_width = line_width_max - (max_min_width_delta * minimized_fraction)
|
|
||||||
fax = bax + (self.width - line_width) * progress
|
|
||||||
fbx = fax + line_width
|
|
||||||
line_width = line_width - 1
|
|
||||||
else
|
|
||||||
fax, fbx = bax, bax + self.width * progress
|
|
||||||
end
|
|
||||||
|
|
||||||
local foreground_size = fby - fay
|
|
||||||
local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping
|
|
||||||
|
|
||||||
-- time starts 0.5 pixels in
|
|
||||||
local time_ax = bax + 0.5
|
|
||||||
local time_width = self.width - line_width - 1
|
|
||||||
|
|
||||||
-- time to x: calculates x coordinate so that it never lies inside of the line
|
|
||||||
local function t2x(time)
|
|
||||||
local x = time_ax + time_width * time / state.duration
|
|
||||||
return time <= state.time and x or x + line_width
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
ass:new_event()
|
|
||||||
ass:pos(0, 0)
|
|
||||||
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}')
|
|
||||||
ass:opacity(options.timeline_opacity)
|
|
||||||
ass:draw_start()
|
|
||||||
ass:rect_cw(bax, bay, fax, bby) --left of progress
|
|
||||||
ass:rect_cw(fbx, bay, bbx, bby) --right of progress
|
|
||||||
ass:rect_cw(fax, bay, fbx, fay) --above progress
|
|
||||||
ass:draw_stop()
|
|
||||||
|
|
||||||
-- Progress
|
|
||||||
ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity})
|
|
||||||
|
|
||||||
-- Uncached ranges
|
|
||||||
local buffered_playtime = nil
|
|
||||||
if state.uncached_ranges then
|
|
||||||
local opts = {size = 80, anchor_y = fby}
|
|
||||||
local texture_char = visibility > 0 and 'b' or 'a'
|
|
||||||
local offset = opts.size / (visibility > 0 and 24 or 28)
|
|
||||||
for _, range in ipairs(state.uncached_ranges) do
|
|
||||||
if not buffered_playtime and (range[1] > state.time or range[2] > state.time) then
|
|
||||||
buffered_playtime = (range[1] - state.time) / (state.speed or 1)
|
|
||||||
end
|
|
||||||
if options.timeline_cache then
|
|
||||||
local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
|
|
||||||
local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
|
|
||||||
opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax
|
|
||||||
ass:texture(ax, fay, bx, fby, texture_char, opts)
|
|
||||||
opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset
|
|
||||||
ass:texture(ax, fay, bx, fby, texture_char, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Custom ranges
|
|
||||||
for _, chapter_range in ipairs(state.chapter_ranges) do
|
|
||||||
local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start)
|
|
||||||
local rbx = chapter_range['end'] > state.duration - 0.1 and bbx
|
|
||||||
or t2x(math.min(chapter_range['end'], state.duration))
|
|
||||||
ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Chapters
|
|
||||||
local hovered_chapter = nil
|
|
||||||
if (options.timeline_chapters_opacity > 0
|
|
||||||
and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b)
|
|
||||||
) then
|
|
||||||
local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size
|
|
||||||
local diamond_radius_hovered = diamond_radius * 2
|
|
||||||
local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1
|
|
||||||
|
|
||||||
if diamond_radius > 0 then
|
|
||||||
local function draw_chapter(time, radius)
|
|
||||||
local chapter_x, chapter_y = t2x(time), fay - 1
|
|
||||||
ass:new_event()
|
|
||||||
ass:append(string.format(
|
|
||||||
'{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
|
|
||||||
diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
|
|
||||||
))
|
|
||||||
ass:draw_start()
|
|
||||||
ass:move_to(chapter_x - radius, chapter_y)
|
|
||||||
ass:line_to(chapter_x, chapter_y - radius)
|
|
||||||
ass:line_to(chapter_x + radius, chapter_y)
|
|
||||||
ass:line_to(chapter_x, chapter_y + radius)
|
|
||||||
ass:draw_stop()
|
|
||||||
end
|
|
||||||
|
|
||||||
if #state.chapters > 0 then
|
|
||||||
-- Find hovered chapter indicator
|
|
||||||
local closest_delta = INFINITY
|
|
||||||
|
|
||||||
if self.proximity_raw < diamond_radius_hovered then
|
|
||||||
for i, chapter in ipairs(state.chapters) do
|
|
||||||
local chapter_x, chapter_y = t2x(chapter.time), fay - 1
|
|
||||||
local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2)
|
|
||||||
if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then
|
|
||||||
hovered_chapter, closest_delta = chapter, cursor_chapter_delta
|
|
||||||
self.is_hovered = true
|
|
||||||
cursor.on_primary_down = function()
|
|
||||||
mp.commandv('seek', hovered_chapter.time, 'absolute+exact')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for i, chapter in ipairs(state.chapters) do
|
|
||||||
if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Render hovered chapter above others
|
|
||||||
if hovered_chapter then draw_chapter(hovered_chapter.time, diamond_radius_hovered) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A-B loop indicators
|
|
||||||
local has_a, has_b = state.ab_loop_a and state.ab_loop_a >= 0, state.ab_loop_b and state.ab_loop_b > 0
|
|
||||||
local ab_radius = round(math.min(math.max(8, foreground_size * 0.25), foreground_size))
|
|
||||||
|
|
||||||
---@param time number
|
|
||||||
---@param kind 'a'|'b'
|
|
||||||
local function draw_ab_indicator(time, kind)
|
|
||||||
local x = t2x(time)
|
|
||||||
ass:new_event()
|
|
||||||
ass:append(string.format(
|
|
||||||
'{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
|
|
||||||
diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
|
|
||||||
))
|
|
||||||
ass:draw_start()
|
|
||||||
ass:move_to(x, fby - ab_radius)
|
|
||||||
if kind == 'b' then ass:line_to(x + 3, fby - ab_radius) end
|
|
||||||
ass:line_to(x + (kind == 'a' and 0 or ab_radius), fby)
|
|
||||||
ass:line_to(x - (kind == 'b' and 0 or ab_radius), fby)
|
|
||||||
if kind == 'a' then ass:line_to(x - 3, fby - ab_radius) end
|
|
||||||
ass:draw_stop()
|
|
||||||
end
|
|
||||||
|
|
||||||
if has_a then draw_ab_indicator(state.ab_loop_a, 'a') end
|
|
||||||
if has_b then draw_ab_indicator(state.ab_loop_b, 'b') end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function draw_timeline_text(x, y, align, text, opts)
|
|
||||||
opts.color, opts.border_color = fgt, fg
|
|
||||||
opts.clip = '\\clip(' .. foreground_coordinates .. ')'
|
|
||||||
ass:txt(x, y, align, text, opts)
|
|
||||||
opts.color, opts.border_color = bgt, bg
|
|
||||||
opts.clip = '\\iclip(' .. foreground_coordinates .. ')'
|
|
||||||
ass:txt(x, y, align, text, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Time values
|
|
||||||
if text_opacity > 0 then
|
|
||||||
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2}
|
|
||||||
-- Upcoming cache time
|
|
||||||
if buffered_playtime and options.buffered_time_threshold > 0
|
|
||||||
and buffered_playtime < options.buffered_time_threshold then
|
|
||||||
local x, align = fbx + 5, 4
|
|
||||||
local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1}
|
|
||||||
local human = round(math.max(buffered_playtime, 0)) .. 's'
|
|
||||||
local width = text_width(human, cache_opts)
|
|
||||||
local time_width = timestamp_width(state.time_human, time_opts)
|
|
||||||
local time_width_end = timestamp_width(state.destination_time_human, time_opts)
|
|
||||||
local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width_end
|
|
||||||
if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end
|
|
||||||
draw_timeline_text(x, fcy, align, human, cache_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Elapsed time
|
|
||||||
if state.time_human then
|
|
||||||
draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- End time
|
|
||||||
if state.destination_time_human then
|
|
||||||
draw_timeline_text(bbx - spacing, fcy, 6, state.destination_time_human, time_opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hovered time and chapter
|
|
||||||
local rendered_thumbnail = false
|
|
||||||
if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and
|
|
||||||
not (Elements.speed and Elements.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)
|
|
||||||
|
|
||||||
-- Cursor line
|
|
||||||
-- 0.5 to switch when the pixel is half filled in
|
|
||||||
local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg
|
|
||||||
local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby
|
|
||||||
ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2})
|
|
||||||
local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by}
|
|
||||||
|
|
||||||
-- Timestamp
|
|
||||||
local offset = #state.chapters > 0 and 10 or 4
|
|
||||||
local opts = {size = self.font_size, offset = offset}
|
|
||||||
local hovered_time_human = format_time(hovered_seconds, state.duration)
|
|
||||||
opts.width_overwrite = timestamp_width(hovered_time_human, opts)
|
|
||||||
ass:tooltip(tooltip_anchor, hovered_time_human, opts)
|
|
||||||
tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset
|
|
||||||
|
|
||||||
-- Thumbnail
|
|
||||||
if not thumbnail.disabled
|
|
||||||
and (not self.pressed or self.pressed.distance < 5)
|
|
||||||
and thumbnail.width ~= 0
|
|
||||||
and thumbnail.height ~= 0
|
|
||||||
then
|
|
||||||
local scale_x, scale_y = display.scale_x, display.scale_y
|
|
||||||
local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y)
|
|
||||||
local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y
|
|
||||||
local thumb_width, thumb_height = thumbnail.width, thumbnail.height
|
|
||||||
local thumb_x = round(clamp(
|
|
||||||
thumb_x_margin, cursor_x * scale_x - thumb_width / 2,
|
|
||||||
display.width * scale_x - thumb_width - thumb_x_margin
|
|
||||||
))
|
|
||||||
local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height)
|
|
||||||
local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y
|
|
||||||
local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y
|
|
||||||
ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2})
|
|
||||||
mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y)
|
|
||||||
self.has_thumbnail, rendered_thumbnail = true, true
|
|
||||||
tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Chapter title
|
|
||||||
if #state.chapters > 0 then
|
|
||||||
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true)
|
|
||||||
if chapter and not chapter.is_end_only then
|
|
||||||
ass:tooltip(tooltip_anchor, chapter.title_wrapped, {
|
|
||||||
size = self.font_size, offset = 10, responsive = false, bold = true,
|
|
||||||
width_overwrite = chapter.title_wrapped_width * self.font_size,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Clear thumbnail
|
|
||||||
if not rendered_thumbnail and self.has_thumbnail then self:clear_thumbnail() end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return Timeline
|
|
|
@ -1,253 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()}
|
|
||||||
|
|
||||||
---@class TopBarButton : Element
|
|
||||||
local TopBarButton = class(Element)
|
|
||||||
|
|
||||||
---@param id string
|
|
||||||
---@param props TopBarButtonProps
|
|
||||||
function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end
|
|
||||||
function TopBarButton:init(id, props)
|
|
||||||
Element.init(self, id, props)
|
|
||||||
self.anchor_id = 'top_bar'
|
|
||||||
self.icon = props.icon
|
|
||||||
self.background = props.background
|
|
||||||
self.command = props.command
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBarButton:handle_cursor_down()
|
|
||||||
mp.command(type(self.command) == 'function' and self.command() or self.command)
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBarButton:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
if visibility <= 0 then return end
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
-- Background on hover
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility})
|
|
||||||
cursor.on_primary_down = function() self:handle_cursor_down() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local width, height = self.bx - self.ax, self.by - self.ay
|
|
||||||
local icon_size = math.min(width, height) * 0.5
|
|
||||||
ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, {
|
|
||||||
opacity = visibility, border = options.text_border,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ TopBar ]]
|
|
||||||
|
|
||||||
---@class TopBar : Element
|
|
||||||
local TopBar = class(Element)
|
|
||||||
|
|
||||||
function TopBar:new() return Class.new(self) --[[@as TopBar]] end
|
|
||||||
function TopBar:init()
|
|
||||||
Element.init(self, 'top_bar')
|
|
||||||
self.size = 0
|
|
||||||
self.icon_size, self.spacing, self.font_size, self.title_bx, self.title_by = 1, 1, 1, 1, 1
|
|
||||||
self.show_alt_title = false
|
|
||||||
self.main_title, self.alt_title = nil, nil
|
|
||||||
|
|
||||||
local function get_maximized_command()
|
|
||||||
return state.border
|
|
||||||
and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized')
|
|
||||||
or 'set window-maximized no;cycle fullscreen'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Order aligns from right to left
|
|
||||||
self.buttons = {
|
|
||||||
TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}),
|
|
||||||
TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = get_maximized_command}),
|
|
||||||
TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}),
|
|
||||||
}
|
|
||||||
|
|
||||||
self:decide_titles()
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:decide_enabled()
|
|
||||||
if options.top_bar == 'no-border' then
|
|
||||||
self.enabled = not state.border or state.fullscreen
|
|
||||||
else
|
|
||||||
self.enabled = options.top_bar == 'always'
|
|
||||||
end
|
|
||||||
self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title)
|
|
||||||
for _, element in ipairs(self.buttons) do
|
|
||||||
element.enabled = self.enabled and options.top_bar_controls
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:decide_titles()
|
|
||||||
self.alt_title = state.alt_title ~= '' and state.alt_title or nil
|
|
||||||
self.main_title = state.title ~= '' and state.title or nil
|
|
||||||
|
|
||||||
-- Fall back to alt title if main is empty
|
|
||||||
if not self.main_title then
|
|
||||||
self.main_title, self.alt_title = self.alt_title, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Deduplicate the main and alt titles by checking if one completely
|
|
||||||
-- contains the other, and using only the longer one.
|
|
||||||
if self.main_title and self.alt_title and not self.show_alt_title then
|
|
||||||
local longer_title, shorter_title
|
|
||||||
if #self.main_title < #self.alt_title then
|
|
||||||
longer_title, shorter_title = self.alt_title, self.main_title
|
|
||||||
else
|
|
||||||
longer_title, shorter_title = self.main_title, self.alt_title
|
|
||||||
end
|
|
||||||
|
|
||||||
local escaped_shorter_title = string.gsub(shorter_title --[[@as string]], "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1")
|
|
||||||
if string.match(longer_title --[[@as string]], escaped_shorter_title) then
|
|
||||||
self.main_title, self.alt_title = longer_title, nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:update_dimensions()
|
|
||||||
self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size
|
|
||||||
self.icon_size = round(self.size * 0.5)
|
|
||||||
self.spacing = math.ceil(self.size * 0.25)
|
|
||||||
self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale)
|
|
||||||
self.button_width = round(self.size * 1.15)
|
|
||||||
self.ay = Elements.window_border.size
|
|
||||||
self.bx = display.width - Elements.window_border.size
|
|
||||||
self.by = self.size + Elements.window_border.size
|
|
||||||
self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0)
|
|
||||||
self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx
|
|
||||||
|
|
||||||
local button_bx = self.bx
|
|
||||||
for _, element in pairs(self.buttons) do
|
|
||||||
element.ax, element.bx = button_bx - self.button_width, button_bx
|
|
||||||
element.ay, element.by = self.ay, self.by
|
|
||||||
button_bx = button_bx - self.button_width
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:toggle_title()
|
|
||||||
if options.top_bar_alt_title_place ~= 'toggle' then return end
|
|
||||||
self.show_alt_title = not self.show_alt_title
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:on_prop_title() self:decide_titles() end
|
|
||||||
function TopBar:on_prop_alt_title() self:decide_titles() end
|
|
||||||
|
|
||||||
function TopBar:on_prop_border()
|
|
||||||
self:decide_enabled()
|
|
||||||
self:update_dimensions()
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:on_prop_fullscreen()
|
|
||||||
self:decide_enabled()
|
|
||||||
self:update_dimensions()
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:on_prop_maximized()
|
|
||||||
self:decide_enabled()
|
|
||||||
self:update_dimensions()
|
|
||||||
end
|
|
||||||
|
|
||||||
function TopBar:on_display() self:update_dimensions() end
|
|
||||||
|
|
||||||
function TopBar:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
if visibility <= 0 then return end
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
-- Window title
|
|
||||||
if options.top_bar_title and (state.title or state.has_playlist) then
|
|
||||||
local bg_margin = math.floor((self.size - self.font_size) / 4)
|
|
||||||
local padding = self.font_size / 2
|
|
||||||
local title_ax = self.ax + bg_margin
|
|
||||||
local title_ay = self.ay + bg_margin
|
|
||||||
local max_bx = self.title_bx - self.spacing
|
|
||||||
|
|
||||||
-- Playlist position
|
|
||||||
if state.has_playlist then
|
|
||||||
local text = state.playlist_pos .. '' .. state.playlist_count
|
|
||||||
local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/'
|
|
||||||
.. state.playlist_count
|
|
||||||
local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility}
|
|
||||||
local bx = round(title_ax + text_width(text, opts) + padding * 2)
|
|
||||||
ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2})
|
|
||||||
ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts)
|
|
||||||
title_ax = bx + bg_margin
|
|
||||||
local rect = {ax = self.ax, ay = self.ay, bx = bx, by = self.by}
|
|
||||||
|
|
||||||
if get_point_to_rectangle_proximity(cursor, rect) == 0 then
|
|
||||||
cursor.on_primary_down = function() mp.command('script-binding uosc/playlist') end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Skip rendering titles if there's not enough horizontal space
|
|
||||||
if max_bx - title_ax > self.font_size * 3 then
|
|
||||||
-- Main title
|
|
||||||
local main_title = self.show_alt_title and self.alt_title or self.main_title
|
|
||||||
if main_title then
|
|
||||||
local opts = {
|
|
||||||
size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility,
|
|
||||||
clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by),
|
|
||||||
}
|
|
||||||
local bx = math.min(max_bx, title_ax + text_width(main_title, opts) + padding * 2)
|
|
||||||
local by = self.by - bg_margin
|
|
||||||
local rect = {ax = title_ax, ay = self.ay, bx = self.title_bx, by = self.by}
|
|
||||||
|
|
||||||
if get_point_to_rectangle_proximity(cursor, rect) == 0 then
|
|
||||||
cursor.on_primary_down = function() self:toggle_title() end
|
|
||||||
end
|
|
||||||
|
|
||||||
ass:rect(title_ax, title_ay, bx, by, {
|
|
||||||
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
|
|
||||||
})
|
|
||||||
ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, main_title, opts)
|
|
||||||
title_ay = by + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Alt title
|
|
||||||
if self.alt_title and options.top_bar_alt_title_place == 'below' then
|
|
||||||
local font_size = self.font_size * 0.9
|
|
||||||
local height = font_size * 1.3
|
|
||||||
local by = title_ay + height
|
|
||||||
local opts = {
|
|
||||||
size = font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility
|
|
||||||
}
|
|
||||||
local bx = math.min(max_bx, title_ax + text_width(self.alt_title, opts) + padding * 2)
|
|
||||||
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
|
|
||||||
ass:rect(title_ax, title_ay, bx, by, {
|
|
||||||
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
|
|
||||||
})
|
|
||||||
ass:txt(title_ax + padding, title_ay + height / 2, 4, self.alt_title, opts)
|
|
||||||
title_ay = by + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Subtitle: current chapter
|
|
||||||
if state.current_chapter then
|
|
||||||
local font_size = self.font_size * 0.8
|
|
||||||
local height = font_size * 1.3
|
|
||||||
local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title
|
|
||||||
local by = title_ay + height
|
|
||||||
local opts = {
|
|
||||||
size = font_size, italic = true, wrap = 2, color = bgt,
|
|
||||||
border = 1, border_color = bg, opacity = visibility * 0.8,
|
|
||||||
}
|
|
||||||
local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2)
|
|
||||||
opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by)
|
|
||||||
ass:rect(title_ax, title_ay, bx, by, {
|
|
||||||
color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2,
|
|
||||||
})
|
|
||||||
ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts)
|
|
||||||
title_ay = by + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.title_by = title_ay - 1
|
|
||||||
else
|
|
||||||
self.title_by = self.ay
|
|
||||||
end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
return TopBar
|
|
|
@ -1,252 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
--[[ MuteButton ]]
|
|
||||||
|
|
||||||
---@class MuteButton : Element
|
|
||||||
local MuteButton = class(Element)
|
|
||||||
---@param props? ElementProps
|
|
||||||
function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end
|
|
||||||
function MuteButton:get_visibility() return Elements.volume:get_visibility(self) end
|
|
||||||
function MuteButton:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
if visibility <= 0 then return end
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end
|
|
||||||
end
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
local icon_name = state.mute and 'volume_off' or 'volume_up'
|
|
||||||
local width = self.bx - self.ax
|
|
||||||
ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name,
|
|
||||||
{border = options.text_border, opacity = options.volume_opacity * visibility, align = 2}
|
|
||||||
)
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ VolumeSlider ]]
|
|
||||||
|
|
||||||
---@class VolumeSlider : Element
|
|
||||||
local VolumeSlider = class(Element)
|
|
||||||
---@param props? ElementProps
|
|
||||||
function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end
|
|
||||||
function VolumeSlider:init(props)
|
|
||||||
Element.init(self, 'volume_slider', props)
|
|
||||||
self.pressed = false
|
|
||||||
self.nudge_y = 0 -- vertical position where volume overflows 100
|
|
||||||
self.nudge_size = 0
|
|
||||||
self.draw_nudge = false
|
|
||||||
self.spacing = 0
|
|
||||||
self.radius = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function VolumeSlider:get_visibility() return Elements.volume:get_visibility(self) end
|
|
||||||
|
|
||||||
function VolumeSlider:set_volume(volume)
|
|
||||||
volume = round(volume / options.volume_step) * options.volume_step
|
|
||||||
if state.volume == volume then return end
|
|
||||||
mp.commandv('set', 'volume', clamp(0, volume, state.volume_max))
|
|
||||||
end
|
|
||||||
|
|
||||||
function VolumeSlider:set_from_cursor()
|
|
||||||
local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border)
|
|
||||||
self:set_volume(volume_fraction * state.volume_max)
|
|
||||||
end
|
|
||||||
|
|
||||||
function VolumeSlider:on_coordinates()
|
|
||||||
if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end
|
|
||||||
local width = self.bx - self.ax
|
|
||||||
self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max))
|
|
||||||
self.nudge_size = round(width * 0.18)
|
|
||||||
self.draw_nudge = self.ay < self.nudge_y
|
|
||||||
self.spacing = round(width * 0.2)
|
|
||||||
self.radius = math.max(2, (self.bx - self.ax) / 10)
|
|
||||||
end
|
|
||||||
function VolumeSlider:on_global_mouse_move()
|
|
||||||
if self.pressed then self:set_from_cursor() end
|
|
||||||
end
|
|
||||||
function VolumeSlider:handle_wheel_up() self:set_volume(state.volume + options.volume_step) end
|
|
||||||
function VolumeSlider:handle_wheel_down() self:set_volume(state.volume - options.volume_step) end
|
|
||||||
|
|
||||||
function VolumeSlider:render()
|
|
||||||
local visibility = self:get_visibility()
|
|
||||||
local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by
|
|
||||||
local width, height = bx - ax, by - ay
|
|
||||||
|
|
||||||
if width <= 0 or height <= 0 or visibility <= 0 then return end
|
|
||||||
|
|
||||||
if self.proximity_raw == 0 then
|
|
||||||
cursor.on_primary_down = function()
|
|
||||||
self.pressed = true
|
|
||||||
self:set_from_cursor()
|
|
||||||
cursor.on_primary_up = function() self.pressed = false end
|
|
||||||
end
|
|
||||||
cursor.on_wheel_down = function() self:handle_wheel_down() end
|
|
||||||
cursor.on_wheel_up = function() self:handle_wheel_up() end
|
|
||||||
end
|
|
||||||
if self.pressed then cursor.on_primary_up = function()
|
|
||||||
self.pressed = false end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -INFINITY, self.nudge_size
|
|
||||||
local volume_y = self.ay + options.volume_border +
|
|
||||||
((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
|
|
||||||
|
|
||||||
-- Draws a rectangle with nudge at requested position
|
|
||||||
---@param p number Padding from slider edges.
|
|
||||||
---@param cy? number A y coordinate where to clip the path from the bottom.
|
|
||||||
function create_nudged_path(p, cy)
|
|
||||||
cy = cy or ay + p
|
|
||||||
local ax, bx, by = ax + p, bx - p, by - p
|
|
||||||
local r = math.max(1, self.radius - p)
|
|
||||||
local d, rh = r * 2, r / 2
|
|
||||||
local nudge_size = ((QUARTER_PI_SIN * (nudge_size - p)) + p) / QUARTER_PI_SIN
|
|
||||||
local path = assdraw.ass_new()
|
|
||||||
path:move_to(bx - r, by)
|
|
||||||
path:line_to(ax + r, by)
|
|
||||||
if cy > by - d then
|
|
||||||
local subtracted_radius = (d - (cy - (by - d))) / 2
|
|
||||||
local xbd = (r - subtracted_radius * 1.35) -- x bezier delta
|
|
||||||
path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy)
|
|
||||||
path:line_to(bx - r, cy)
|
|
||||||
path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by)
|
|
||||||
else
|
|
||||||
path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r)
|
|
||||||
local nudge_bottom_y = nudge_y + nudge_size
|
|
||||||
|
|
||||||
if cy + rh <= nudge_bottom_y then
|
|
||||||
path:line_to(ax, nudge_bottom_y)
|
|
||||||
if cy <= nudge_y then
|
|
||||||
path:line_to((ax + nudge_size), nudge_y)
|
|
||||||
local nudge_top_y = nudge_y - nudge_size
|
|
||||||
if cy <= nudge_top_y then
|
|
||||||
local r, rh = r, rh
|
|
||||||
if cy > nudge_top_y - r then
|
|
||||||
r = nudge_top_y - cy
|
|
||||||
rh = r / 2
|
|
||||||
end
|
|
||||||
path:line_to(ax, nudge_top_y)
|
|
||||||
path:line_to(ax, cy + r)
|
|
||||||
path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
|
|
||||||
path:line_to(bx - r, cy)
|
|
||||||
path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
|
|
||||||
path:line_to(bx, nudge_top_y)
|
|
||||||
else
|
|
||||||
local triangle_side = cy - nudge_top_y
|
|
||||||
path:line_to((ax + triangle_side), cy)
|
|
||||||
path:line_to((bx - triangle_side), cy)
|
|
||||||
end
|
|
||||||
path:line_to((bx - nudge_size), nudge_y)
|
|
||||||
else
|
|
||||||
local triangle_side = nudge_bottom_y - cy
|
|
||||||
path:line_to((ax + triangle_side), cy)
|
|
||||||
path:line_to((bx - triangle_side), cy)
|
|
||||||
end
|
|
||||||
path:line_to(bx, nudge_bottom_y)
|
|
||||||
else
|
|
||||||
path:line_to(ax, cy + r)
|
|
||||||
path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
|
|
||||||
path:line_to(bx - r, cy)
|
|
||||||
path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
|
|
||||||
end
|
|
||||||
path:line_to(bx, by - r)
|
|
||||||
path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by)
|
|
||||||
end
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
-- BG & FG paths
|
|
||||||
local bg_path = create_nudged_path(0)
|
|
||||||
local fg_path = create_nudged_path(options.volume_border, volume_y)
|
|
||||||
|
|
||||||
-- Background
|
|
||||||
ass:new_event()
|
|
||||||
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg ..
|
|
||||||
'\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}')
|
|
||||||
ass:opacity(options.volume_opacity, visibility)
|
|
||||||
ass:pos(0, 0)
|
|
||||||
ass:draw_start()
|
|
||||||
ass:append(bg_path.text)
|
|
||||||
ass:draw_stop()
|
|
||||||
|
|
||||||
-- Foreground
|
|
||||||
ass:new_event()
|
|
||||||
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}')
|
|
||||||
ass:opacity(options.volume_opacity, visibility)
|
|
||||||
ass:pos(0, 0)
|
|
||||||
ass:draw_start()
|
|
||||||
ass:append(fg_path.text)
|
|
||||||
ass:draw_stop()
|
|
||||||
|
|
||||||
-- Current volume value
|
|
||||||
local volume_string = tostring(round(state.volume * 10) / 10)
|
|
||||||
local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale)
|
|
||||||
if volume_y < self.by - self.spacing then
|
|
||||||
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
|
|
||||||
size = font_size, color = fgt, opacity = visibility,
|
|
||||||
clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
if volume_y > self.by - self.spacing - font_size then
|
|
||||||
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
|
|
||||||
size = font_size, color = bgt, opacity = visibility,
|
|
||||||
clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Disabled stripes for no audio
|
|
||||||
if not state.has_audio then
|
|
||||||
local fg_100_path = create_nudged_path(options.volume_border)
|
|
||||||
local texture_opts = {
|
|
||||||
size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax,
|
|
||||||
clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')',
|
|
||||||
}
|
|
||||||
ass:texture(ax, ay, bx, by, 'a', texture_opts)
|
|
||||||
texture_opts.color = '000000'
|
|
||||||
texture_opts.anchor_x = ax + texture_opts.size / 28
|
|
||||||
ass:texture(ax, ay, bx, by, 'a', texture_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ Volume ]]
|
|
||||||
|
|
||||||
---@class Volume : Element
|
|
||||||
local Volume = class(Element)
|
|
||||||
|
|
||||||
function Volume:new() return Class.new(self) --[[@as Volume]] end
|
|
||||||
function Volume:init()
|
|
||||||
Element.init(self, 'volume')
|
|
||||||
self.mute = MuteButton:new({anchor_id = 'volume'})
|
|
||||||
self.slider = VolumeSlider:new({anchor_id = 'volume'})
|
|
||||||
end
|
|
||||||
|
|
||||||
function Volume:get_visibility()
|
|
||||||
return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Volume:update_dimensions()
|
|
||||||
local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size
|
|
||||||
local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar
|
|
||||||
local min_y = top_bar.enabled and top_bar.by or 0
|
|
||||||
local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay)
|
|
||||||
or display.height - top_bar.size
|
|
||||||
local available_height = max_y - min_y
|
|
||||||
local max_height = available_height * 0.8
|
|
||||||
local height = round(math.min(width * 8, max_height))
|
|
||||||
self.enabled = height > width * 2 -- don't render if too small
|
|
||||||
local margin = (width / 2) + Elements.window_border.size
|
|
||||||
self.ax = round(options.volume == 'left' and margin or display.width - margin - width)
|
|
||||||
self.ay = min_y + round((available_height - height) / 2)
|
|
||||||
self.bx = round(self.ax + width)
|
|
||||||
self.by = round(self.ay + height)
|
|
||||||
self.mute.enabled, self.slider.enabled = self.enabled, self.enabled
|
|
||||||
self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by)
|
|
||||||
self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Volume:on_display() self:update_dimensions() end
|
|
||||||
function Volume:on_prop_border() self:update_dimensions() end
|
|
||||||
function Volume:on_controls_reflow() self:update_dimensions() end
|
|
||||||
|
|
||||||
return Volume
|
|
|
@ -1,33 +0,0 @@
|
||||||
local Element = require('uosc_shared/elements/Element')
|
|
||||||
|
|
||||||
---@class WindowBorder : Element
|
|
||||||
local WindowBorder = class(Element)
|
|
||||||
|
|
||||||
function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end
|
|
||||||
function WindowBorder:init()
|
|
||||||
Element.init(self, 'window_border')
|
|
||||||
self.ignores_menu = true
|
|
||||||
self.size = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function WindowBorder:decide_enabled()
|
|
||||||
self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border
|
|
||||||
self.size = self.enabled and options.window_border_size or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function WindowBorder:on_prop_border() self:decide_enabled() end
|
|
||||||
function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end
|
|
||||||
|
|
||||||
function WindowBorder:render()
|
|
||||||
if self.size > 0 then
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
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 = options.window_border_opacity,
|
|
||||||
})
|
|
||||||
return ass
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return WindowBorder
|
|
|
@ -1,170 +0,0 @@
|
||||||
--[[ ASSDRAW EXTENSIONS ]]
|
|
||||||
|
|
||||||
local ass_mt = getmetatable(assdraw.ass_new())
|
|
||||||
|
|
||||||
-- Opacity.
|
|
||||||
---@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities.
|
|
||||||
---@param fraction? number Optionally adjust the above opacity by this fraction.
|
|
||||||
function ass_mt:opacity(opacity, fraction)
|
|
||||||
fraction = fraction ~= nil and fraction or 1
|
|
||||||
if type(opacity) == 'number' then
|
|
||||||
self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
|
|
||||||
else
|
|
||||||
self.text = self.text .. string.format(
|
|
||||||
'{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
|
|
||||||
opacity_to_alpha((opacity[1] or 0) * fraction),
|
|
||||||
opacity_to_alpha((opacity[2] or 0) * fraction),
|
|
||||||
opacity_to_alpha((opacity[3] or 0) * fraction),
|
|
||||||
opacity_to_alpha((opacity[4] or 0) * fraction)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Icon.
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param size number
|
|
||||||
---@param name string
|
|
||||||
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number}
|
|
||||||
function ass_mt:icon(x, y, size, name, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false
|
|
||||||
self:txt(x, y, opts.align or 5, name, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Text.
|
|
||||||
-- Named `txt` because `ass.text` is a value.
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param align number
|
|
||||||
---@param value string|number
|
|
||||||
---@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string}
|
|
||||||
function ass_mt:txt(x, y, align, value, opts)
|
|
||||||
local border_size = opts.border or 0
|
|
||||||
local shadow_size = opts.shadow or 0
|
|
||||||
local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0'
|
|
||||||
-- font
|
|
||||||
tags = tags .. '\\fn' .. (opts.font or config.font)
|
|
||||||
-- font size
|
|
||||||
tags = tags .. '\\fs' .. opts.size
|
|
||||||
-- bold
|
|
||||||
if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end
|
|
||||||
-- italic
|
|
||||||
if opts.italic then tags = tags .. '\\i1' end
|
|
||||||
-- rotate
|
|
||||||
if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end
|
|
||||||
-- wrap
|
|
||||||
if opts.wrap then tags = tags .. '\\q' .. opts.wrap end
|
|
||||||
-- border
|
|
||||||
tags = tags .. '\\bord' .. border_size
|
|
||||||
-- shadow
|
|
||||||
tags = tags .. '\\shad' .. shadow_size
|
|
||||||
-- colors
|
|
||||||
tags = tags .. '\\1c&H' .. (opts.color or bgt)
|
|
||||||
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
|
|
||||||
if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end
|
|
||||||
-- opacity
|
|
||||||
if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end
|
|
||||||
-- clip
|
|
||||||
if opts.clip then tags = tags .. opts.clip end
|
|
||||||
-- render
|
|
||||||
self:new_event()
|
|
||||||
self.text = self.text .. '{' .. tags .. '}' .. value
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Tooltip.
|
|
||||||
---@param element {ax: number; ay: number; bx: number; by: number}
|
|
||||||
---@param value string|number
|
|
||||||
---@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean}
|
|
||||||
function ass_mt:tooltip(element, value, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
opts.size = opts.size or 16
|
|
||||||
opts.border = options.text_border
|
|
||||||
opts.border_color = bg
|
|
||||||
local offset = opts.offset or opts.size / 2
|
|
||||||
local align_top = opts.responsive == false or element.ay - offset > opts.size * 2
|
|
||||||
local x = element.ax + (element.bx - element.ax) / 2
|
|
||||||
local y = align_top and element.ay - offset or element.by + offset
|
|
||||||
local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10
|
|
||||||
self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Rectangle.
|
|
||||||
---@param ax number
|
|
||||||
---@param ay number
|
|
||||||
---@param bx number
|
|
||||||
---@param by number
|
|
||||||
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number}
|
|
||||||
function ass_mt:rect(ax, ay, bx, by, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local border_size = opts.border or 0
|
|
||||||
local tags = '\\pos(0,0)\\rDefault\\an7\\blur0'
|
|
||||||
-- border
|
|
||||||
tags = tags .. '\\bord' .. border_size
|
|
||||||
-- colors
|
|
||||||
tags = tags .. '\\1c&H' .. (opts.color or fg)
|
|
||||||
if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end
|
|
||||||
-- opacity
|
|
||||||
if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end
|
|
||||||
if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end
|
|
||||||
-- clip
|
|
||||||
if opts.clip then
|
|
||||||
tags = tags .. opts.clip
|
|
||||||
end
|
|
||||||
-- draw
|
|
||||||
self:new_event()
|
|
||||||
self.text = self.text .. '{' .. tags .. '}'
|
|
||||||
self:draw_start()
|
|
||||||
if opts.radius then
|
|
||||||
self:round_rect_cw(ax, ay, bx, by, opts.radius)
|
|
||||||
else
|
|
||||||
self:rect_cw(ax, ay, bx, by)
|
|
||||||
end
|
|
||||||
self:draw_stop()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Circle.
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param radius number
|
|
||||||
---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string}
|
|
||||||
function ass_mt:circle(x, y, radius, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
opts.radius = radius
|
|
||||||
self:rect(x - radius, y - radius, x + radius, y + radius, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Texture.
|
|
||||||
---@param ax number
|
|
||||||
---@param ay number
|
|
||||||
---@param bx number
|
|
||||||
---@param by number
|
|
||||||
---@param char string Texture font character.
|
|
||||||
---@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number}
|
|
||||||
function ass_mt:texture(ax, ay, bx, by, char, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay
|
|
||||||
local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')')
|
|
||||||
local tile_size, opacity = opts.size or 100, opts.opacity or 0.2
|
|
||||||
local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size
|
|
||||||
local width, height = bx - x, by - y
|
|
||||||
local line = string.rep(char, math.ceil((width / tile_size)))
|
|
||||||
local lines = ''
|
|
||||||
for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end
|
|
||||||
self:txt(
|
|
||||||
x, y, 7, lines,
|
|
||||||
{font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Rotating spinner icon.
|
|
||||||
---@param x number
|
|
||||||
---@param y number
|
|
||||||
---@param size number
|
|
||||||
---@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;}
|
|
||||||
function ass_mt:spinner(x, y, size, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
opts.rotate = (state.render_last_time * 1.75 % 1) * -360
|
|
||||||
opts.color = opts.color or fg
|
|
||||||
self:icon(x, y, size, 'autorenew', opts)
|
|
||||||
request_render()
|
|
||||||
end
|
|
|
@ -1,292 +0,0 @@
|
||||||
---@param data MenuData
|
|
||||||
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
|
|
||||||
function open_command_menu(data, opts)
|
|
||||||
local function run_command(command)
|
|
||||||
if type(command) == 'string' then
|
|
||||||
mp.command(command)
|
|
||||||
else
|
|
||||||
---@diagnostic disable-next-line: deprecated
|
|
||||||
mp.commandv(unpack(command))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
---@type MenuOptions
|
|
||||||
local menu_opts = {}
|
|
||||||
if opts then
|
|
||||||
menu_opts.mouse_nav = opts.mouse_nav
|
|
||||||
if opts.on_close then menu_opts.on_close = function() run_command(opts.on_close) end end
|
|
||||||
end
|
|
||||||
local menu = Menu:open(data, run_command, menu_opts)
|
|
||||||
if opts and opts.submenu then menu:activate_submenu(opts.submenu) end
|
|
||||||
return menu
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts? {submenu?: string; mouse_nav?: boolean; on_close?: string | string[]}
|
|
||||||
function toggle_menu_with_items(opts)
|
|
||||||
if Menu:is_open('menu') then Menu:close()
|
|
||||||
else open_command_menu({type = 'menu', items = config.menu_items}, opts) end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any); on_move_item?: fun(from_index: integer, to_index: integer, submenu_path: integer[]); on_delete_item?: fun(index: integer, submenu_path: integer[])}
|
|
||||||
function create_self_updating_menu_opener(options)
|
|
||||||
return function()
|
|
||||||
if Menu:is_open(options.type) then Menu:close() return end
|
|
||||||
local list = mp.get_property_native(options.list_prop)
|
|
||||||
local active = options.active_prop and mp.get_property_native(options.active_prop) or nil
|
|
||||||
local menu
|
|
||||||
|
|
||||||
local function update() menu:update_items(options.serializer(list, active)) end
|
|
||||||
|
|
||||||
local ignore_initial_list = true
|
|
||||||
local function handle_list_prop_change(name, value)
|
|
||||||
if ignore_initial_list then ignore_initial_list = false
|
|
||||||
else list = value update() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ignore_initial_active = true
|
|
||||||
local function handle_active_prop_change(name, value)
|
|
||||||
if ignore_initial_active then ignore_initial_active = false
|
|
||||||
else active = value update() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local initial_items, selected_index = options.serializer(list, active)
|
|
||||||
|
|
||||||
-- Items and active_index are set in the handle_prop_change callback, since adding
|
|
||||||
-- a property observer triggers its handler immediately, we just let that initialize the items.
|
|
||||||
menu = Menu:open(
|
|
||||||
{type = options.type, title = options.title, items = initial_items, selected_index = selected_index},
|
|
||||||
options.on_select, {
|
|
||||||
on_open = function()
|
|
||||||
mp.observe_property(options.list_prop, 'native', handle_list_prop_change)
|
|
||||||
if options.active_prop then
|
|
||||||
mp.observe_property(options.active_prop, 'native', handle_active_prop_change)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
on_close = function()
|
|
||||||
mp.unobserve_property(handle_list_prop_change)
|
|
||||||
mp.unobserve_property(handle_active_prop_change)
|
|
||||||
end,
|
|
||||||
on_move_item = options.on_move_item,
|
|
||||||
on_delete_item = options.on_delete_item,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command)
|
|
||||||
local function serialize_tracklist(tracklist)
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
if load_command then
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local first_item_index = #items + 1
|
|
||||||
local active_index = nil
|
|
||||||
local disabled_item = nil
|
|
||||||
|
|
||||||
-- Add option to disable a subtitle track. This works for all tracks,
|
|
||||||
-- but why would anyone want to disable audio or video? Better to not
|
|
||||||
-- let people mistakenly select what is unwanted 99.999% of the time.
|
|
||||||
-- If I'm mistaken and there is an active need for this, feel free to
|
|
||||||
-- open an issue.
|
|
||||||
if track_type == 'sub' then
|
|
||||||
disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true}
|
|
||||||
items[#items + 1] = disabled_item
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, track in ipairs(tracklist) do
|
|
||||||
if track.type == track_type then
|
|
||||||
local hint_values = {}
|
|
||||||
local function h(value) hint_values[#hint_values + 1] = value end
|
|
||||||
|
|
||||||
if track.lang then h(track.lang:upper()) end
|
|
||||||
if track['demux-h'] then
|
|
||||||
h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p'))
|
|
||||||
end
|
|
||||||
if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end
|
|
||||||
h(track.codec)
|
|
||||||
if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end
|
|
||||||
if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end
|
|
||||||
if track.forced then h('forced') end
|
|
||||||
if track.default then h('default') end
|
|
||||||
if track.external then h('external') end
|
|
||||||
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = (track.title and track.title or 'Track ' .. track.id),
|
|
||||||
hint = table.concat(hint_values, ', '),
|
|
||||||
value = track.id,
|
|
||||||
active = track.selected,
|
|
||||||
}
|
|
||||||
|
|
||||||
if track.selected then
|
|
||||||
if disabled_item then disabled_item.active = false end
|
|
||||||
active_index = #items
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return items, active_index or first_item_index
|
|
||||||
end
|
|
||||||
|
|
||||||
local function selection_handler(value)
|
|
||||||
if value == '{load}' then
|
|
||||||
mp.command(load_command)
|
|
||||||
else
|
|
||||||
mp.commandv('set', track_prop, value and value or 'no')
|
|
||||||
|
|
||||||
-- If subtitle track was selected, assume user also wants to see it
|
|
||||||
if value and track_type == 'sub' then
|
|
||||||
mp.commandv('set', 'sub-visibility', 'yes')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return create_self_updating_menu_opener({
|
|
||||||
title = menu_title,
|
|
||||||
type = track_type,
|
|
||||||
list_prop = 'track-list',
|
|
||||||
serializer = serialize_tracklist,
|
|
||||||
on_select = selection_handler,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()}
|
|
||||||
|
|
||||||
-- Opens a file navigation menu with items inside `directory_path`.
|
|
||||||
---@param directory_path string
|
|
||||||
---@param handle_select fun(path: string): nil
|
|
||||||
---@param opts NavigationMenuOptions
|
|
||||||
function open_file_navigation_menu(directory_path, handle_select, opts)
|
|
||||||
directory = serialize_path(normalize_path(directory_path))
|
|
||||||
opts = opts or {}
|
|
||||||
|
|
||||||
if not directory then
|
|
||||||
msg.error('Couldn\'t serialize path "' .. directory_path .. '.')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local files, directories = read_directory(directory.path, opts.allowed_types)
|
|
||||||
local is_root = not directory.dirname
|
|
||||||
local path_separator = path_separator(directory.path)
|
|
||||||
|
|
||||||
if not files or not directories then return end
|
|
||||||
|
|
||||||
sort_filenames(directories)
|
|
||||||
sort_filenames(files)
|
|
||||||
|
|
||||||
-- Pre-populate items with parent directory selector if not at root
|
|
||||||
-- Each item value is a serialized path table it points to.
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
if is_root then
|
|
||||||
if state.platform == 'windows' then
|
|
||||||
items[#items + 1] = {title = '..', hint = 'Drives', value = '{drives}', separator = true}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
items[#items + 1] = {title = '..', hint = 'parent dir', value = directory.dirname, separator = true}
|
|
||||||
end
|
|
||||||
|
|
||||||
local back_path = items[#items] and items[#items].value
|
|
||||||
local selected_index = #items + 1
|
|
||||||
|
|
||||||
for _, dir in ipairs(directories) do
|
|
||||||
items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator}
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, file in ipairs(files) do
|
|
||||||
items[#items + 1] = {title = file, value = join_path(directory.path, file)}
|
|
||||||
end
|
|
||||||
|
|
||||||
for index, item in ipairs(items) do
|
|
||||||
if not item.value.is_to_parent and opts.active_path == item.value then
|
|
||||||
item.active = true
|
|
||||||
if not opts.selected_path then selected_index = index end
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts.selected_path == item.value then selected_index = index end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type MenuCallback
|
|
||||||
local function open_path(path, meta)
|
|
||||||
local is_drives = path == '{drives}'
|
|
||||||
local is_to_parent = is_drives or #path < #directory_path
|
|
||||||
local inheritable_options = {
|
|
||||||
type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_drives then
|
|
||||||
open_drives_menu(function(drive_path)
|
|
||||||
open_file_navigation_menu(drive_path, handle_select, inheritable_options)
|
|
||||||
end, {
|
|
||||||
type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path,
|
|
||||||
on_open = opts.on_open, on_close = opts.on_close,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local info, error = utils.file_info(path)
|
|
||||||
|
|
||||||
if not info then
|
|
||||||
msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or ''))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if info.is_dir and not meta.modifiers.ctrl then
|
|
||||||
-- Preselect directory we are coming from
|
|
||||||
if is_to_parent then
|
|
||||||
inheritable_options.selected_path = directory.path
|
|
||||||
end
|
|
||||||
|
|
||||||
open_file_navigation_menu(path, handle_select, inheritable_options)
|
|
||||||
else
|
|
||||||
handle_select(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handle_back()
|
|
||||||
if back_path then open_path(back_path, {modifiers = {}}) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local menu_data = {
|
|
||||||
type = opts.type, title = opts.title or directory.basename .. path_separator, items = items,
|
|
||||||
selected_index = selected_index,
|
|
||||||
}
|
|
||||||
local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back}
|
|
||||||
|
|
||||||
return Menu:open(menu_data, open_path, menu_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Opens a file navigation menu with Windows drives as items.
|
|
||||||
---@param handle_select fun(path: string): nil
|
|
||||||
---@param opts? NavigationMenuOptions
|
|
||||||
function open_drives_menu(handle_select, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local process = mp.command_native({
|
|
||||||
name = 'subprocess',
|
|
||||||
capture_stdout = true,
|
|
||||||
playback_only = false,
|
|
||||||
args = {'wmic', 'logicaldisk', 'get', 'name', '/value'},
|
|
||||||
})
|
|
||||||
local items, selected_index = {}, 1
|
|
||||||
|
|
||||||
if process.status == 0 then
|
|
||||||
for _, value in ipairs(split(process.stdout, '\n')) do
|
|
||||||
local drive = string.match(value, 'Name=([A-Z]:)')
|
|
||||||
if drive then
|
|
||||||
local drive_path = normalize_path(drive)
|
|
||||||
items[#items + 1] = {
|
|
||||||
title = drive, hint = 'drive', value = drive_path, active = opts.active_path == drive_path,
|
|
||||||
}
|
|
||||||
if opts.selected_path == drive_path then selected_index = #items end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
msg.error(process.stderr)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Menu:open(
|
|
||||||
{type = opts.type, title = opts.title or 'Drives', items = items, selected_index = selected_index},
|
|
||||||
handle_select
|
|
||||||
)
|
|
||||||
end
|
|
|
@ -1,181 +0,0 @@
|
||||||
--[[ Stateless utilities missing in lua standard library ]]
|
|
||||||
|
|
||||||
---@param number number
|
|
||||||
function round(number) return math.floor(number + 0.5) end
|
|
||||||
|
|
||||||
---@param min number
|
|
||||||
---@param value number
|
|
||||||
---@param max number
|
|
||||||
function clamp(min, value, max) return math.max(min, math.min(value, max)) end
|
|
||||||
|
|
||||||
---@param rgba string `rrggbb` or `rrggbbaa` hex string.
|
|
||||||
function serialize_rgba(rgba)
|
|
||||||
local a = rgba:sub(7, 8)
|
|
||||||
return {
|
|
||||||
color = rgba:sub(5, 6) .. rgba:sub(3, 4) .. rgba:sub(1, 2),
|
|
||||||
opacity = clamp(0, tonumber(#a == 2 and a or 'ff', 16) / 255, 1),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Trim any `char` from the end of the string.
|
|
||||||
---@param str string
|
|
||||||
---@param char string
|
|
||||||
---@return string
|
|
||||||
function trim_end(str, char)
|
|
||||||
local char, end_i = char:byte(), 0
|
|
||||||
for i = #str, 1, -1 do
|
|
||||||
if str:byte(i) ~= char then
|
|
||||||
end_i = i
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return str:sub(1, end_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param str string
|
|
||||||
---@param pattern string
|
|
||||||
---@return string[]
|
|
||||||
function split(str, pattern)
|
|
||||||
local list = {}
|
|
||||||
local full_pattern = '(.-)' .. pattern
|
|
||||||
local last_end = 1
|
|
||||||
local start_index, end_index, capture = str:find(full_pattern, 1)
|
|
||||||
while start_index do
|
|
||||||
list[#list + 1] = capture
|
|
||||||
last_end = end_index + 1
|
|
||||||
start_index, end_index, capture = str:find(full_pattern, last_end)
|
|
||||||
end
|
|
||||||
if last_end <= (#str + 1) then
|
|
||||||
capture = str:sub(last_end)
|
|
||||||
list[#list + 1] = capture
|
|
||||||
end
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Get index of the last appearance of `sub` in `str`.
|
|
||||||
---@param str string
|
|
||||||
---@param sub string
|
|
||||||
---@return integer|nil
|
|
||||||
function string_last_index_of(str, sub)
|
|
||||||
local sub_length = #sub
|
|
||||||
for i = #str, 1, -1 do
|
|
||||||
for j = 1, sub_length do
|
|
||||||
if str:byte(i + j - 1) ~= sub:byte(j) then break end
|
|
||||||
if j == sub_length then return i end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param itable table
|
|
||||||
---@param value any
|
|
||||||
---@return integer|nil
|
|
||||||
function itable_index_of(itable, value)
|
|
||||||
for index, item in ipairs(itable) do
|
|
||||||
if item == value then return index end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param itable table
|
|
||||||
---@param compare fun(value: any, index: number)
|
|
||||||
---@param from_end? boolean Search from the end of the table.
|
|
||||||
---@return number|nil index
|
|
||||||
---@return any|nil value
|
|
||||||
function itable_find(itable, compare, from_end)
|
|
||||||
local from, to, step = from_end and #itable or 1, from_end and 1 or #itable, from_end and -1 or 1
|
|
||||||
for index = from, to, step do
|
|
||||||
if compare(itable[index], index) then return index, itable[index] end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param itable table
|
|
||||||
---@param decider fun(value: any, index: number)
|
|
||||||
function itable_filter(itable, decider)
|
|
||||||
local filtered = {}
|
|
||||||
for index, value in ipairs(itable) do
|
|
||||||
if decider(value, index) then filtered[#filtered + 1] = value end
|
|
||||||
end
|
|
||||||
return filtered
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param itable table
|
|
||||||
---@param value any
|
|
||||||
function itable_remove(itable, value)
|
|
||||||
return itable_filter(itable, function(item) return item ~= value end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param itable table
|
|
||||||
---@param start_pos? integer
|
|
||||||
---@param end_pos? integer
|
|
||||||
function itable_slice(itable, start_pos, end_pos)
|
|
||||||
start_pos = start_pos and start_pos or 1
|
|
||||||
end_pos = end_pos and end_pos or #itable
|
|
||||||
|
|
||||||
if end_pos < 0 then end_pos = #itable + end_pos + 1 end
|
|
||||||
if start_pos < 0 then start_pos = #itable + start_pos + 1 end
|
|
||||||
|
|
||||||
local new_table = {}
|
|
||||||
for index, value in ipairs(itable) do
|
|
||||||
if index >= start_pos and index <= end_pos then
|
|
||||||
new_table[#new_table + 1] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return new_table
|
|
||||||
end
|
|
||||||
|
|
||||||
---@generic T
|
|
||||||
---@param a T[]|nil
|
|
||||||
---@param b T[]|nil
|
|
||||||
---@return T[]
|
|
||||||
function itable_join(a, b)
|
|
||||||
local result = {}
|
|
||||||
if a then for _, value in ipairs(a) do result[#result + 1] = value end end
|
|
||||||
if b then for _, value in ipairs(b) do result[#result + 1] = value end end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param target any[]
|
|
||||||
---@param source any[]
|
|
||||||
function itable_append(target, source)
|
|
||||||
for _, value in ipairs(source) do target[#target + 1] = value end
|
|
||||||
return target
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param target any[]
|
|
||||||
---@param source any[]
|
|
||||||
---@param props? string[]
|
|
||||||
function table_assign(target, source, props)
|
|
||||||
if props then
|
|
||||||
for _, name in ipairs(props) do target[name] = source[name] end
|
|
||||||
else
|
|
||||||
for prop, value in pairs(source) do target[prop] = value end
|
|
||||||
end
|
|
||||||
return target
|
|
||||||
end
|
|
||||||
|
|
||||||
---@generic T
|
|
||||||
---@param table T
|
|
||||||
---@return T
|
|
||||||
function table_shallow_copy(table)
|
|
||||||
local result = {}
|
|
||||||
for key, value in pairs(table) do result[key] = value end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ EASING FUNCTIONS ]]
|
|
||||||
|
|
||||||
function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end
|
|
||||||
function ease_out_sext(x) return 1 - ((1 - x) ^ 6) end
|
|
||||||
|
|
||||||
--[[ CLASSES ]]
|
|
||||||
|
|
||||||
---@class Class
|
|
||||||
Class = {}
|
|
||||||
function Class:new(...)
|
|
||||||
local object = setmetatable({}, {__index = self})
|
|
||||||
object:init(...)
|
|
||||||
return object
|
|
||||||
end
|
|
||||||
function Class:init() end
|
|
||||||
function Class:destroy() end
|
|
||||||
|
|
||||||
function class(parent) return setmetatable({}, {__index = parent or Class}) end
|
|
|
@ -1,461 +0,0 @@
|
||||||
-- https://en.wikipedia.org/wiki/Unicode_block
|
|
||||||
---@alias CodePointRange {[1]: integer; [2]: integer}
|
|
||||||
|
|
||||||
---@type CodePointRange[]
|
|
||||||
local zero_width_blocks = {
|
|
||||||
{0x0000, 0x001F}, -- C0
|
|
||||||
{0x007F, 0x009F}, -- Delete + C1
|
|
||||||
{0x034F, 0x034F}, -- combining grapheme joiner
|
|
||||||
{0x061C, 0x061C}, -- Arabic Letter Strong
|
|
||||||
{0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark}
|
|
||||||
{0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override}
|
|
||||||
{0x2060, 0x2060}, -- word joiner
|
|
||||||
{0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate}
|
|
||||||
{0xFEFF, 0xFEFF}, -- zero-width non-breaking space
|
|
||||||
-- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character
|
|
||||||
{0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited
|
|
||||||
{0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited
|
|
||||||
{0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited
|
|
||||||
{0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited
|
|
||||||
{0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters)
|
|
||||||
-- Egyptian Hieroglyph Format Controls and Shorthand format Controls
|
|
||||||
{0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs
|
|
||||||
{0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common
|
|
||||||
-- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters
|
|
||||||
{0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- All characters have the same width as the first one
|
|
||||||
---@type CodePointRange[]
|
|
||||||
local same_width_blocks = {
|
|
||||||
{0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han
|
|
||||||
{0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han
|
|
||||||
{0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han
|
|
||||||
{0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han
|
|
||||||
{0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han
|
|
||||||
{0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E 2 SIP Han
|
|
||||||
{0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F 2 SIP Han
|
|
||||||
{0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement 2 SIP Han
|
|
||||||
{0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G 3 TIP Han
|
|
||||||
{0x31350, 0x323AF}, -- CJK Unified Ideographs Extension H 3 TIP Han
|
|
||||||
}
|
|
||||||
|
|
||||||
local width_length_ratio = 0.5
|
|
||||||
|
|
||||||
---@type integer, integer
|
|
||||||
local osd_width, osd_height = 100, 100
|
|
||||||
|
|
||||||
---Get byte count of utf-8 character at index i in str
|
|
||||||
---@param str string
|
|
||||||
---@param i integer?
|
|
||||||
---@return integer
|
|
||||||
local function utf8_char_bytes(str, i)
|
|
||||||
local char_byte = str:byte(i)
|
|
||||||
if char_byte < 0xC0 then return 1
|
|
||||||
elseif char_byte < 0xE0 then return 2
|
|
||||||
elseif char_byte < 0xF0 then return 3
|
|
||||||
elseif char_byte < 0xF8 then return 4
|
|
||||||
else return 1 end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Creates an iterator for an utf-8 encoded string
|
|
||||||
---Iterates over utf-8 characters instead of bytes
|
|
||||||
---@param str string
|
|
||||||
---@return fun(): integer?, string?
|
|
||||||
local function utf8_iter(str)
|
|
||||||
local byte_start = 1
|
|
||||||
return function()
|
|
||||||
local start = byte_start
|
|
||||||
if #str < start then return nil end
|
|
||||||
local byte_count = utf8_char_bytes(str, start)
|
|
||||||
byte_start = start + byte_count
|
|
||||||
return start, str:sub(start, start + byte_count - 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Extract Unicode code point from utf-8 character at index i in str
|
|
||||||
---@param str string
|
|
||||||
---@param i integer
|
|
||||||
---@return integer
|
|
||||||
local function utf8_to_unicode(str, i)
|
|
||||||
local byte_count = utf8_char_bytes(str, i)
|
|
||||||
local char_byte = str:byte(i)
|
|
||||||
local unicode = char_byte
|
|
||||||
if byte_count ~= 1 then
|
|
||||||
local shift = 2 ^ (8 - byte_count)
|
|
||||||
char_byte = char_byte - math.floor(0xFF / shift) * shift
|
|
||||||
unicode = char_byte * (2 ^ 6) ^ (byte_count - 1)
|
|
||||||
end
|
|
||||||
for j = 2, byte_count do
|
|
||||||
char_byte = str:byte(i + j - 1) - 0x80
|
|
||||||
unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j)
|
|
||||||
end
|
|
||||||
return round(unicode)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Convert Unicode code point to utf-8 string
|
|
||||||
---@param unicode integer
|
|
||||||
---@return string?
|
|
||||||
local function unicode_to_utf8(unicode)
|
|
||||||
if unicode < 0x80 then return string.char(unicode)
|
|
||||||
else
|
|
||||||
local byte_count
|
|
||||||
if unicode < 0x800 then byte_count = 2
|
|
||||||
elseif unicode < 0x10000 then byte_count = 3
|
|
||||||
elseif unicode < 0x110000 then byte_count = 4
|
|
||||||
else return end -- too big
|
|
||||||
|
|
||||||
local res = {}
|
|
||||||
local shift = 2 ^ 6
|
|
||||||
local after_shift = unicode
|
|
||||||
for _ = byte_count, 2, -1 do
|
|
||||||
local before_shift = after_shift
|
|
||||||
after_shift = math.floor(before_shift / shift)
|
|
||||||
table.insert(res, 1, before_shift - after_shift * shift + 0x80)
|
|
||||||
end
|
|
||||||
shift = 2 ^ (8 - byte_count)
|
|
||||||
table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift)
|
|
||||||
---@diagnostic disable-next-line: deprecated
|
|
||||||
return string.char(unpack(res))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Update osd resolution if valid
|
|
||||||
---@param width integer
|
|
||||||
---@param height integer
|
|
||||||
local function update_osd_resolution(width, height)
|
|
||||||
if width > 0 and height > 0 then osd_width, osd_height = width, height end
|
|
||||||
end
|
|
||||||
|
|
||||||
mp.observe_property('osd-dimensions', 'native', function (_, dim)
|
|
||||||
if dim then update_osd_resolution(dim.w, dim.h) end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local measure_bounds
|
|
||||||
do
|
|
||||||
local text_osd = mp.create_osd_overlay("ass-events")
|
|
||||||
text_osd.compute_bounds, text_osd.hidden = true, true
|
|
||||||
|
|
||||||
---@param ass_text string
|
|
||||||
---@return integer, integer, integer, integer
|
|
||||||
measure_bounds = function(ass_text)
|
|
||||||
update_osd_resolution(mp.get_osd_size())
|
|
||||||
text_osd.res_x, text_osd.res_y = osd_width, osd_height
|
|
||||||
text_osd.data = ass_text
|
|
||||||
local res = text_osd:update()
|
|
||||||
return res.x0, res.y0, res.x1, res.y1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local normalized_text_width
|
|
||||||
do
|
|
||||||
---@type {wrap: integer; bold: boolean; italic: boolean, rotate: number; size: number}
|
|
||||||
local bounds_opts = {wrap = 2, bold = false, italic = false, rotate = 0, size = 0}
|
|
||||||
|
|
||||||
---Measure text width and normalize to a font size of 1
|
|
||||||
---text has to be ass safe
|
|
||||||
---@param text string
|
|
||||||
---@param size number
|
|
||||||
---@param bold boolean
|
|
||||||
---@param italic boolean
|
|
||||||
---@param horizontal boolean
|
|
||||||
---@return number, integer
|
|
||||||
normalized_text_width = function(text, size, bold, italic, horizontal)
|
|
||||||
bounds_opts.bold, bounds_opts.italic, bounds_opts.rotate = bold, italic, horizontal and 0 or -90
|
|
||||||
local x1, y1 = nil, nil
|
|
||||||
size = size / 0.8
|
|
||||||
-- prevent endless loop
|
|
||||||
local repetitions_left = 5
|
|
||||||
repeat
|
|
||||||
size = size * 0.8
|
|
||||||
bounds_opts.size = size
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
ass:txt(0, 0, horizontal and 7 or 1, text, bounds_opts)
|
|
||||||
_, _, x1, y1 = measure_bounds(ass.text)
|
|
||||||
repetitions_left = repetitions_left - 1
|
|
||||||
-- make sure nothing got clipped
|
|
||||||
until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0
|
|
||||||
local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1)
|
|
||||||
return width / size, horizontal and osd_width or osd_height
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Estimates character length based on utf8 byte count
|
|
||||||
---1 character length is roughly the size of a latin character
|
|
||||||
---@param char string
|
|
||||||
---@return number
|
|
||||||
local function char_length(char)
|
|
||||||
return #char > 2 and 2 or 1
|
|
||||||
end
|
|
||||||
|
|
||||||
---Estimates string length based on utf8 byte count
|
|
||||||
---Note: Making a string in the iterator with the character is a waste here,
|
|
||||||
---but as this function is only used when measuring whole string widths it's fine
|
|
||||||
---@param text string
|
|
||||||
---@return number
|
|
||||||
local function text_length(text)
|
|
||||||
if not text or text == '' then return 0 end
|
|
||||||
local text_length = 0
|
|
||||||
for _, char in utf8_iter(tostring(text)) do text_length = text_length + char_length(char) end
|
|
||||||
return text_length
|
|
||||||
end
|
|
||||||
|
|
||||||
---Finds the best orientation of text on screen and returns the estimated max size
|
|
||||||
---and if the text should be drawn horizontally
|
|
||||||
---@param text string
|
|
||||||
---@return number, boolean
|
|
||||||
local function fit_on_screen(text)
|
|
||||||
local estimated_width = text_length(text) * width_length_ratio
|
|
||||||
if osd_width >= osd_height then
|
|
||||||
-- Fill the screen as much as we can, bigger is more accurate.
|
|
||||||
return math.min(osd_width / estimated_width, osd_height), true
|
|
||||||
else
|
|
||||||
return math.min(osd_height / estimated_width, osd_width), false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Gets next stage from cache
|
|
||||||
---@param cache {[any]: table}
|
|
||||||
---@param value any
|
|
||||||
local function get_cache_stage(cache, value)
|
|
||||||
local stage = cache[value]
|
|
||||||
if not stage then
|
|
||||||
stage = {}
|
|
||||||
cache[value] = stage
|
|
||||||
end
|
|
||||||
return stage
|
|
||||||
end
|
|
||||||
|
|
||||||
---Is measured resolution sufficient
|
|
||||||
---@param px integer
|
|
||||||
---@return boolean
|
|
||||||
local function no_remeasure_required(px)
|
|
||||||
return px >= 800 or (px * 1.1 >= osd_width and px * 1.1 >= osd_height)
|
|
||||||
end
|
|
||||||
|
|
||||||
local character_width
|
|
||||||
do
|
|
||||||
---@type {[boolean]: {[string]: {[1]: number, [2]: integer}}}
|
|
||||||
local char_width_cache = {}
|
|
||||||
|
|
||||||
---Get measured width of character
|
|
||||||
---@param char string
|
|
||||||
---@param bold boolean
|
|
||||||
---@return number, integer
|
|
||||||
character_width = function(char, bold)
|
|
||||||
---@type {[string]: {[1]: number, [2]: integer}}
|
|
||||||
local char_widths = get_cache_stage(char_width_cache, bold)
|
|
||||||
local width_px = char_widths[char]
|
|
||||||
if width_px and no_remeasure_required(width_px[2]) then return width_px[1], width_px[2] end
|
|
||||||
|
|
||||||
local unicode = utf8_to_unicode(char, 1)
|
|
||||||
for _, block in ipairs(zero_width_blocks) do
|
|
||||||
if unicode >= block[1] and unicode <= block[2] then
|
|
||||||
char_widths[char] = {0, INFINITY}
|
|
||||||
return 0, INFINITY
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local measured_char = nil
|
|
||||||
for _, block in ipairs(same_width_blocks) do
|
|
||||||
if unicode >= block[1] and unicode <= block[2] then
|
|
||||||
measured_char = unicode_to_utf8(block[1])
|
|
||||||
width_px = char_widths[measured_char]
|
|
||||||
if width_px and no_remeasure_required(width_px[2]) then
|
|
||||||
char_widths[char] = width_px
|
|
||||||
return width_px[1], width_px[2]
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not measured_char then measured_char = char end
|
|
||||||
-- half as many repetitions for wide characters
|
|
||||||
local char_count = 10 / char_length(char)
|
|
||||||
local max_size, horizontal = fit_on_screen(measured_char:rep(char_count))
|
|
||||||
local size = math.min(max_size * 0.9, 50)
|
|
||||||
char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100)
|
|
||||||
local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count
|
|
||||||
if measured_char == enclosing_char then enclosing_char = ''
|
|
||||||
else enclosing_width = 2 * character_width(enclosing_char, bold) end
|
|
||||||
local width_ratio, width, px = nil, nil, nil
|
|
||||||
repeat
|
|
||||||
char_count = next_char_count
|
|
||||||
local str = enclosing_char .. measured_char:rep(char_count) .. enclosing_char
|
|
||||||
width, px = normalized_text_width(str, size, bold, false, horizontal)
|
|
||||||
width = width - enclosing_width
|
|
||||||
width_ratio = width * size / (horizontal and osd_width or osd_height)
|
|
||||||
next_char_count = math.min(math.floor(char_count / width_ratio * 0.9), 100)
|
|
||||||
until width_ratio < 0.05 or width_ratio > 0.5 or char_count == next_char_count
|
|
||||||
width = width / char_count
|
|
||||||
|
|
||||||
width_px = {width, px}
|
|
||||||
if char ~= measured_char then char_widths[measured_char] = width_px end
|
|
||||||
char_widths[char] = width_px
|
|
||||||
return width, px
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Calculate text width from individual measured characters
|
|
||||||
---@param text string|number
|
|
||||||
---@param bold boolean
|
|
||||||
---@return number, integer
|
|
||||||
local function character_based_width(text, bold)
|
|
||||||
local max_width = 0
|
|
||||||
local min_px = INFINITY
|
|
||||||
for line in tostring(text):gmatch("([^\n]*)\n?") do
|
|
||||||
local total_width = 0
|
|
||||||
for _, char in utf8_iter(line) do
|
|
||||||
local width, px = character_width(char, bold)
|
|
||||||
total_width = total_width + width
|
|
||||||
if px < min_px then min_px = px end
|
|
||||||
end
|
|
||||||
if total_width > max_width then max_width = total_width end
|
|
||||||
end
|
|
||||||
return max_width, min_px
|
|
||||||
end
|
|
||||||
|
|
||||||
---Measure width of whole text
|
|
||||||
---@param text string|number
|
|
||||||
---@param bold boolean
|
|
||||||
---@param italic boolean
|
|
||||||
---@return number, integer
|
|
||||||
local function whole_text_width(text, bold, italic)
|
|
||||||
text = tostring(text)
|
|
||||||
local size, horizontal = fit_on_screen(text)
|
|
||||||
return normalized_text_width(ass_escape(text), size * 0.9, bold, italic, horizontal)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Scale normalized width to real width based on font size and italic
|
|
||||||
---@param opts {size: number; italic?: boolean}
|
|
||||||
---@return number, number
|
|
||||||
local function opts_factor_offset(opts)
|
|
||||||
return opts.size, opts.italic and opts.size * 0.2 or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
---Scale normalized width to real width based on font size and italic
|
|
||||||
---@param opts {size: number; italic?: boolean}
|
|
||||||
---@return number
|
|
||||||
local function normalized_to_real(width, opts)
|
|
||||||
local factor, offset = opts_factor_offset(opts)
|
|
||||||
return factor * width + offset
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
---@type {[boolean]: {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}} | {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}
|
|
||||||
local width_cache = {}
|
|
||||||
|
|
||||||
---Calculate width of text with the given opts
|
|
||||||
---@param text string|number
|
|
||||||
---@return number
|
|
||||||
---@param opts {size: number; bold?: boolean; italic?: boolean}
|
|
||||||
function text_width(text, opts)
|
|
||||||
if not text or text == '' then return 0 end
|
|
||||||
|
|
||||||
---@type boolean, boolean
|
|
||||||
local bold, italic = opts.bold or options.font_bold, opts.italic or false
|
|
||||||
|
|
||||||
if options.text_width_estimation then
|
|
||||||
---@type {[string|number]: {[1]: number, [2]: integer}}
|
|
||||||
local text_width = get_cache_stage(width_cache, bold)
|
|
||||||
local width_px = text_width[text]
|
|
||||||
if width_px and no_remeasure_required(width_px[2]) then return normalized_to_real(width_px[1], opts) end
|
|
||||||
|
|
||||||
local width, px = character_based_width(text, bold)
|
|
||||||
width_cache[bold][text] = {width, px}
|
|
||||||
return normalized_to_real(width, opts)
|
|
||||||
else
|
|
||||||
---@type {[string|number]: {[1]: number, [2]: integer}}
|
|
||||||
local text_width = get_cache_stage(get_cache_stage(width_cache, bold), italic)
|
|
||||||
local width_px = text_width[text]
|
|
||||||
if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts.size end
|
|
||||||
|
|
||||||
local width, px = whole_text_width(text, bold, italic)
|
|
||||||
width_cache[bold][italic][text] = {width, px}
|
|
||||||
return width * opts.size
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
---@type {[string]: string}
|
|
||||||
local cache = {}
|
|
||||||
|
|
||||||
---Get width of formatted timestamp as if all the digits were replaced with 0
|
|
||||||
---@param timestamp string
|
|
||||||
---@param opts {size: number; bold?: boolean; italic?: boolean}
|
|
||||||
---@return number
|
|
||||||
function timestamp_width(timestamp, opts)
|
|
||||||
local substitute = cache[#timestamp]
|
|
||||||
if not substitute then
|
|
||||||
substitute = timestamp:gsub('%d', '0')
|
|
||||||
cache[#timestamp] = substitute
|
|
||||||
end
|
|
||||||
return text_width(substitute, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Wrap the text at the closest opportunity to target_line_length
|
|
||||||
---@param text string
|
|
||||||
---@param opts {size: number; bold?: boolean; italic?: boolean}
|
|
||||||
---@param target_line_length number
|
|
||||||
---@return string
|
|
||||||
function wrap_text(text, opts, target_line_length)
|
|
||||||
local target_line_width = target_line_length * width_length_ratio * opts.size
|
|
||||||
local bold, scale_factor, scale_offset = opts.bold or false, opts_factor_offset(opts)
|
|
||||||
local wrap_at_chars = {' ', ' ', '-', '–'}
|
|
||||||
local remove_when_wrap = {' ', ' '}
|
|
||||||
local lines = {}
|
|
||||||
for text_line in text:gmatch("([^\n]*)\n?") do
|
|
||||||
local line_width = scale_offset
|
|
||||||
local line_start = 1
|
|
||||||
local before_end = nil
|
|
||||||
local before_width = scale_offset
|
|
||||||
local before_line_start = 0
|
|
||||||
local before_removed_width = 0
|
|
||||||
for char_start, char in utf8_iter(text_line) do
|
|
||||||
local char_end = char_start + #char - 1
|
|
||||||
local can_wrap = false
|
|
||||||
for _, c in ipairs(wrap_at_chars) do
|
|
||||||
if char == c then
|
|
||||||
can_wrap = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local char_width = character_width(char, bold) * scale_factor
|
|
||||||
line_width = line_width + char_width
|
|
||||||
if can_wrap or (char_end == #text_line) then
|
|
||||||
local remove = false
|
|
||||||
for _, c in ipairs(remove_when_wrap) do
|
|
||||||
if char == c then
|
|
||||||
remove = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local line_width_after_remove = line_width - (remove and char_width or 0)
|
|
||||||
if line_width_after_remove < target_line_width then
|
|
||||||
before_end = remove and char_start - 1 or char_end
|
|
||||||
before_width = line_width_after_remove
|
|
||||||
before_line_start = char_end + 1
|
|
||||||
before_removed_width = remove and char_width or 0
|
|
||||||
else
|
|
||||||
if (target_line_width - before_width) <
|
|
||||||
(line_width_after_remove - target_line_width) then
|
|
||||||
lines[#lines + 1] = text_line:sub(line_start, before_end)
|
|
||||||
line_start = before_line_start
|
|
||||||
line_width = line_width - before_width - before_removed_width + scale_offset
|
|
||||||
else
|
|
||||||
lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end)
|
|
||||||
line_start = char_end + 1
|
|
||||||
line_width = scale_offset
|
|
||||||
end
|
|
||||||
before_end = line_start
|
|
||||||
before_width = scale_offset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start)
|
|
||||||
elseif text_line == '' then lines[#lines + 1] = '' end
|
|
||||||
end
|
|
||||||
return table.concat(lines, '\n')
|
|
||||||
end
|
|
|
@ -1,609 +0,0 @@
|
||||||
--[[ UI specific utilities that might or might not depend on its state or options ]]
|
|
||||||
|
|
||||||
-- Sorting comparator close to (but not exactly) how file explorers sort files.
|
|
||||||
sort_filenames = (function()
|
|
||||||
local symbol_order
|
|
||||||
local default_order
|
|
||||||
|
|
||||||
if state.platform == 'windows' then
|
|
||||||
symbol_order = {
|
|
||||||
['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
|
|
||||||
['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
|
|
||||||
['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
|
|
||||||
}
|
|
||||||
default_order = 21
|
|
||||||
else
|
|
||||||
symbol_order = {
|
|
||||||
['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
|
|
||||||
['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
|
|
||||||
['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
|
|
||||||
}
|
|
||||||
default_order = 21
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Alphanumeric sorting for humans in Lua
|
|
||||||
-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
|
|
||||||
local function pad_number(n, d)
|
|
||||||
return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
|
|
||||||
or ("%03d%s"):format(#n, n)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- In place sorting of filenames
|
|
||||||
---@param filenames string[]
|
|
||||||
return function(filenames)
|
|
||||||
local tuples = {}
|
|
||||||
for i, filename in ipairs(filenames) do
|
|
||||||
local first_char = filename:sub(1, 1)
|
|
||||||
local order = symbol_order[first_char] or default_order
|
|
||||||
local formatted = filename:lower():gsub('0*(%d+)%.?(%d*)', pad_number)
|
|
||||||
tuples[i] = {order, formatted, filename}
|
|
||||||
end
|
|
||||||
table.sort(tuples, function(a, b)
|
|
||||||
if a[1] ~= b[1] then return a[1] < b[1] end
|
|
||||||
return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2]
|
|
||||||
end)
|
|
||||||
for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end
|
|
||||||
end
|
|
||||||
end)()
|
|
||||||
|
|
||||||
-- Creates in-between frames to animate value from `from` to `to` numbers.
|
|
||||||
---@param from number
|
|
||||||
---@param to number|fun():number
|
|
||||||
---@param setter fun(value: number)
|
|
||||||
---@param factor_or_callback? number|fun()
|
|
||||||
---@param callback? fun() Called either on animation end, or when animation is killed.
|
|
||||||
function tween(from, to, setter, factor_or_callback, callback)
|
|
||||||
local factor = factor_or_callback
|
|
||||||
if type(factor_or_callback) == 'function' then callback = factor_or_callback end
|
|
||||||
if type(factor) ~= 'number' then factor = 0.3 end
|
|
||||||
|
|
||||||
local current, done, timeout = from, false, nil
|
|
||||||
local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end
|
|
||||||
local cutoff = math.abs(get_to() - from) * 0.01
|
|
||||||
|
|
||||||
local function finish()
|
|
||||||
if not done then
|
|
||||||
done = true
|
|
||||||
timeout:kill()
|
|
||||||
if callback then callback() end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function tick()
|
|
||||||
local to = get_to()
|
|
||||||
current = current + ((to - current) * factor)
|
|
||||||
local is_end = math.abs(to - current) <= cutoff
|
|
||||||
setter(is_end and to or current)
|
|
||||||
request_render()
|
|
||||||
if is_end then finish()
|
|
||||||
else timeout:resume() end
|
|
||||||
end
|
|
||||||
|
|
||||||
timeout = mp.add_timeout(state.render_delay, tick)
|
|
||||||
tick()
|
|
||||||
|
|
||||||
return finish
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param point {x: number; y: number}
|
|
||||||
---@param rect {ax: number; ay: number; bx: number; by: number}
|
|
||||||
function get_point_to_rectangle_proximity(point, rect)
|
|
||||||
local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx)
|
|
||||||
local dy = math.max(rect.ay - point.y, 0, point.y - rect.by)
|
|
||||||
return math.sqrt(dx * dx + dy * dy)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param point_a {x: number; y: number}
|
|
||||||
---@param point_b {x: number; y: number}
|
|
||||||
function get_point_to_point_proximity(point_a, point_b)
|
|
||||||
local dx, dy = point_a.x - point_b.x, point_a.y - point_b.y
|
|
||||||
return math.sqrt(dx * dx + dy * dy)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Call function with args if it exists
|
|
||||||
function call_maybe(fn, ...)
|
|
||||||
if type(fn) == 'function' then fn(...) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extracts the properties used by property expansion of that string.
|
|
||||||
---@param str string
|
|
||||||
---@param res { [string] : boolean } | nil
|
|
||||||
---@return { [string] : boolean }
|
|
||||||
function get_expansion_props(str, res)
|
|
||||||
res = res or {}
|
|
||||||
for str in str:gmatch('%$(%b{})') do
|
|
||||||
local name, str = str:match('^{[?!]?=?([^:]+):?(.*)}$')
|
|
||||||
if name then
|
|
||||||
local s = name:find('==') or nil
|
|
||||||
if s then name = name:sub(0, s - 1) end
|
|
||||||
res[name] = true
|
|
||||||
if str and str ~= '' then get_expansion_props(str, res) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Escape a string for verbatim display on the OSD.
|
|
||||||
---@param str string
|
|
||||||
function ass_escape(str)
|
|
||||||
-- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
|
|
||||||
-- it isn't followed by a recognized character, so add a zero-width
|
|
||||||
-- non-breaking space
|
|
||||||
str = str:gsub('\\', '\\\239\187\191')
|
|
||||||
str = str:gsub('{', '\\{')
|
|
||||||
str = str:gsub('}', '\\}')
|
|
||||||
-- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
|
|
||||||
-- consecutive newlines
|
|
||||||
str = str:gsub('\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')
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param seconds number
|
|
||||||
---@param max_seconds number|nil Trims unnecessary `00:` if time is not expected to reach it.
|
|
||||||
---@return string
|
|
||||||
function format_time(seconds, max_seconds)
|
|
||||||
local human = mp.format_time(seconds)
|
|
||||||
if options.time_precision > 0 then
|
|
||||||
local formatted = string.format('%.' .. options.time_precision .. 'f', math.abs(seconds) % 1)
|
|
||||||
human = human .. '.' .. string.sub(formatted, 3)
|
|
||||||
end
|
|
||||||
if max_seconds then
|
|
||||||
local trim_length = (max_seconds < 60 and 7 or (max_seconds < 3600 and 4 or 0))
|
|
||||||
if trim_length > 0 then
|
|
||||||
local has_minus = seconds < 0
|
|
||||||
human = string.sub(human, trim_length + (has_minus and 1 or 0))
|
|
||||||
if has_minus then human = '-' .. human end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return human
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opacity number 0-1
|
|
||||||
function opacity_to_alpha(opacity)
|
|
||||||
return 255 - math.ceil(255 * opacity)
|
|
||||||
end
|
|
||||||
|
|
||||||
path_separator = (function()
|
|
||||||
local os_separator = state.platform == 'windows' and '\\' or '/'
|
|
||||||
|
|
||||||
-- Get appropriate path separator for the given path.
|
|
||||||
---@param path string
|
|
||||||
---@return string
|
|
||||||
return function(path)
|
|
||||||
return path:sub(1, 2) == '\\\\' and '\\' or os_separator
|
|
||||||
end
|
|
||||||
end)()
|
|
||||||
|
|
||||||
-- Joins paths with the OS aware path separator or UNC separator.
|
|
||||||
---@param p1 string
|
|
||||||
---@param p2 string
|
|
||||||
---@return string
|
|
||||||
function join_path(p1, p2)
|
|
||||||
local p1, separator = trim_trailing_separator(p1)
|
|
||||||
-- Prevents joining drive letters with a redundant separator (`C:\\foo`),
|
|
||||||
-- as `trim_trailing_separator()` doesn't trim separators from drive letters.
|
|
||||||
return p1:sub(#p1) == separator and p1 .. p2 or p1 .. separator.. p2
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if path is absolute.
|
|
||||||
---@param path string
|
|
||||||
---@return boolean
|
|
||||||
function is_absolute(path)
|
|
||||||
if path:sub(1, 2) == '\\\\' then return true
|
|
||||||
elseif state.platform == 'windows' then return path:find('^%a+:') ~= nil
|
|
||||||
else return path:sub(1, 1) == '/' end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ensure path is absolute.
|
|
||||||
---@param path string
|
|
||||||
---@return string
|
|
||||||
function ensure_absolute(path)
|
|
||||||
if is_absolute(path) then return path end
|
|
||||||
return join_path(state.cwd, path)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Remove trailing slashes/backslashes.
|
|
||||||
---@param path string
|
|
||||||
---@return string path, string trimmed_separator_type
|
|
||||||
function trim_trailing_separator(path)
|
|
||||||
local separator = path_separator(path)
|
|
||||||
path = trim_end(path, separator)
|
|
||||||
if state.platform == 'windows' then
|
|
||||||
-- Drive letters on windows need trailing backslash
|
|
||||||
if path:sub(#path) == ':' then path = path .. '\\' end
|
|
||||||
else
|
|
||||||
if path == '' then path = '/' end
|
|
||||||
end
|
|
||||||
return path, separator
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ensures path is absolute, remove trailing slashes/backslashes.
|
|
||||||
-- Lightweight version of normalize_path for performance critical parts.
|
|
||||||
---@param path string
|
|
||||||
---@return string
|
|
||||||
function normalize_path_lite(path)
|
|
||||||
if not path or is_protocol(path) then return path end
|
|
||||||
path = trim_trailing_separator(ensure_absolute(path))
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ensures path is absolute, remove trailing slashes/backslashes, normalization of path separators and deduplication.
|
|
||||||
---@param path string
|
|
||||||
---@return string
|
|
||||||
function normalize_path(path)
|
|
||||||
if not path or is_protocol(path) then return path end
|
|
||||||
|
|
||||||
path = ensure_absolute(path)
|
|
||||||
local is_unc = path:sub(1, 2) == '\\\\'
|
|
||||||
if state.platform == 'windows' or is_unc then path = path:gsub('/', '\\') end
|
|
||||||
path = trim_trailing_separator(path)
|
|
||||||
|
|
||||||
--Deduplication of path separators
|
|
||||||
if is_unc then path = path:gsub('(.\\)\\+', '%1')
|
|
||||||
elseif state.platform == 'windows' then path = path:gsub('\\\\+', '\\')
|
|
||||||
else path = path:gsub('//+', '/') end
|
|
||||||
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if path is a protocol, such as `http://...`.
|
|
||||||
---@param path string
|
|
||||||
function is_protocol(path)
|
|
||||||
return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param path string
|
|
||||||
---@param extensions string[] Lowercase extensions without the dot.
|
|
||||||
function has_any_extension(path, extensions)
|
|
||||||
local path_last_dot_index = string_last_index_of(path, '.')
|
|
||||||
if not path_last_dot_index then return false end
|
|
||||||
local path_extension = path:sub(path_last_dot_index + 1):lower()
|
|
||||||
for _, extension in ipairs(extensions) do
|
|
||||||
if path_extension == extension then return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
function get_default_directory()
|
|
||||||
return mp.command_native({'expand-path', options.default_directory})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Serializes path into its semantic parts.
|
|
||||||
---@param path string
|
|
||||||
---@return nil|{path: string; is_root: boolean; dirname?: string; basename: string; filename: string; extension?: string;}
|
|
||||||
function serialize_path(path)
|
|
||||||
if not path or is_protocol(path) then return end
|
|
||||||
|
|
||||||
local normal_path = normalize_path_lite(path)
|
|
||||||
local dirname, basename = utils.split_path(normal_path)
|
|
||||||
if basename == '' then basename, dirname = dirname:sub(1, #dirname - 1), nil end
|
|
||||||
local dot_i = string_last_index_of(basename, '.')
|
|
||||||
|
|
||||||
return {
|
|
||||||
path = normal_path,
|
|
||||||
is_root = dirname == nil,
|
|
||||||
dirname = dirname,
|
|
||||||
basename = basename,
|
|
||||||
filename = dot_i and basename:sub(1, dot_i - 1) or basename,
|
|
||||||
extension = dot_i and basename:sub(dot_i + 1) or nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Reads items in directory and splits it into directories and files tables.
|
|
||||||
---@param path string
|
|
||||||
---@param allowed_types? string[] Filter `files` table to contain only files with these extensions.
|
|
||||||
---@return string[]|nil files
|
|
||||||
---@return string[]|nil directories
|
|
||||||
function read_directory(path, allowed_types)
|
|
||||||
local items, error = utils.readdir(path, 'all')
|
|
||||||
|
|
||||||
if not items then
|
|
||||||
msg.error('Reading files from "' .. path .. '" failed: ' .. error)
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local files, directories = {}, {}
|
|
||||||
|
|
||||||
for _, item in ipairs(items) do
|
|
||||||
if item ~= '.' and item ~= '..' then
|
|
||||||
local info = utils.file_info(join_path(path, item))
|
|
||||||
if info then
|
|
||||||
if info.is_file then
|
|
||||||
if not allowed_types or has_any_extension(item, allowed_types) then
|
|
||||||
files[#files + 1] = item
|
|
||||||
end
|
|
||||||
else directories[#directories + 1] = item end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return files, directories
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Returns full absolute paths of files in the same directory as `file_path`,
|
|
||||||
-- and index of the current file in the table.
|
|
||||||
-- Returned table will always contain `file_path`, regardless of `allowed_types`.
|
|
||||||
---@param file_path string
|
|
||||||
---@param allowed_types? string[] Filter adjacent file types. Does NOT filter out the `file_path`.
|
|
||||||
function get_adjacent_files(file_path, allowed_types)
|
|
||||||
local current_meta = serialize_path(file_path)
|
|
||||||
if not current_meta then return end
|
|
||||||
local files = read_directory(current_meta.dirname)
|
|
||||||
if not files then return end
|
|
||||||
sort_filenames(files)
|
|
||||||
local current_file_index
|
|
||||||
local paths = {}
|
|
||||||
for _, file in ipairs(files) do
|
|
||||||
local is_current_file = current_meta.basename == file
|
|
||||||
if is_current_file or not allowed_types or has_any_extension(file, allowed_types) then
|
|
||||||
paths[#paths + 1] = join_path(current_meta.dirname, file)
|
|
||||||
if is_current_file then current_file_index = #paths end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not current_file_index then return end
|
|
||||||
return paths, current_file_index
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Navigates in a list, using delta or, when `state.shuffle` is enabled,
|
|
||||||
-- randomness to determine the next item. Loops around if `loop-playlist` is enabled.
|
|
||||||
---@param list table
|
|
||||||
---@param current_index number
|
|
||||||
---@param delta number
|
|
||||||
function decide_navigation_in_list(list, current_index, delta)
|
|
||||||
if #list < 2 then return #list, list[#list] end
|
|
||||||
|
|
||||||
if state.shuffle then
|
|
||||||
local new_index = current_index
|
|
||||||
math.randomseed(os.time())
|
|
||||||
while current_index == new_index do new_index = math.random(#list) end
|
|
||||||
return new_index, list[new_index]
|
|
||||||
end
|
|
||||||
|
|
||||||
local new_index = current_index + delta
|
|
||||||
if mp.get_property_native('loop-playlist') then
|
|
||||||
if new_index > #list then new_index = new_index % #list
|
|
||||||
elseif new_index < 1 then new_index = #list - new_index end
|
|
||||||
elseif new_index < 1 or new_index > #list then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return new_index, list[new_index]
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param delta number
|
|
||||||
function navigate_directory(delta)
|
|
||||||
if not state.path or is_protocol(state.path) then return false end
|
|
||||||
local paths, current_index = get_adjacent_files(state.path, config.types.autoload)
|
|
||||||
if paths and current_index then
|
|
||||||
local _, path = decide_navigation_in_list(paths, current_index, delta)
|
|
||||||
if path then mp.commandv('loadfile', path) return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param delta number
|
|
||||||
function navigate_playlist(delta)
|
|
||||||
local playlist, pos = mp.get_property_native('playlist'), mp.get_property_native('playlist-pos-1')
|
|
||||||
if playlist and #playlist > 1 and pos then
|
|
||||||
local index = decide_navigation_in_list(playlist, pos, delta)
|
|
||||||
if index then mp.commandv('playlist-play-index', index - 1) return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param delta number
|
|
||||||
function navigate_item(delta)
|
|
||||||
if state.has_playlist then return navigate_playlist(delta) else return navigate_directory(delta) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Can't use `os.remove()` as it fails on paths with unicode characters.
|
|
||||||
-- Returns `result, error`, result is table of:
|
|
||||||
-- `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean`
|
|
||||||
---@param path string
|
|
||||||
function delete_file(path)
|
|
||||||
if state.platform == 'windows' then
|
|
||||||
if options.use_trash then
|
|
||||||
local ps_code = [[
|
|
||||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
||||||
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('__path__', 'OnlyErrorDialogs', 'SendToRecycleBin')
|
|
||||||
]]
|
|
||||||
|
|
||||||
local escaped_path = string.gsub(path, "'", "''")
|
|
||||||
escaped_path = string.gsub(escaped_path, "’", "’’")
|
|
||||||
escaped_path = string.gsub(escaped_path, "%%", "%%%%")
|
|
||||||
ps_code = string.gsub(ps_code, "__path__", escaped_path)
|
|
||||||
args = { 'powershell', '-NoProfile', '-Command', ps_code }
|
|
||||||
else
|
|
||||||
args = { 'cmd', '/C', 'del', path }
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if options.use_trash then
|
|
||||||
--On Linux and Macos the app trash-cli/trash must be installed first.
|
|
||||||
args = { 'trash', path }
|
|
||||||
else
|
|
||||||
args = { 'rm', path }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return mp.command_native({
|
|
||||||
name = 'subprocess',
|
|
||||||
args = args,
|
|
||||||
playback_only = false,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function serialize_chapter_ranges(normalized_chapters)
|
|
||||||
local ranges = {}
|
|
||||||
local simple_ranges = {
|
|
||||||
{name = 'openings', patterns = {
|
|
||||||
'^op ', '^op$', ' op$',
|
|
||||||
'^opening$', ' opening$'
|
|
||||||
}, requires_next_chapter = true},
|
|
||||||
{name = 'intros', patterns = {
|
|
||||||
'^intro$', ' intro$',
|
|
||||||
'^avant$', '^prologue$'
|
|
||||||
}, requires_next_chapter = true},
|
|
||||||
{name = 'endings', patterns = {
|
|
||||||
'^ed ', '^ed$', ' ed$',
|
|
||||||
'^ending ', '^ending$', ' ending$',
|
|
||||||
}},
|
|
||||||
{name = 'outros', patterns = {
|
|
||||||
'^outro$', ' outro$',
|
|
||||||
'^closing$', '^closing ',
|
|
||||||
'^preview$', '^pv$',
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
local sponsor_ranges = {}
|
|
||||||
|
|
||||||
-- Extend with alt patterns
|
|
||||||
for _, meta in ipairs(simple_ranges) do
|
|
||||||
local alt_patterns = config.chapter_ranges[meta.name] and config.chapter_ranges[meta.name].patterns
|
|
||||||
if alt_patterns then meta.patterns = itable_join(meta.patterns, alt_patterns) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Clone chapters
|
|
||||||
local chapters = {}
|
|
||||||
for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end
|
|
||||||
|
|
||||||
for i, chapter in ipairs(chapters) do
|
|
||||||
-- Simple ranges
|
|
||||||
for _, meta in ipairs(simple_ranges) do
|
|
||||||
if config.chapter_ranges[meta.name] then
|
|
||||||
local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end)
|
|
||||||
if match then
|
|
||||||
local next_chapter = chapters[i + 1]
|
|
||||||
if next_chapter or not meta.requires_next_chapter then
|
|
||||||
ranges[#ranges + 1] = table_assign({
|
|
||||||
start = chapter.time,
|
|
||||||
['end'] = next_chapter and next_chapter.time or INFINITY,
|
|
||||||
}, config.chapter_ranges[meta.name])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sponsor blocks
|
|
||||||
if config.chapter_ranges.ads then
|
|
||||||
local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)')
|
|
||||||
if id then -- ad range from sponsorblock
|
|
||||||
for j = i + 1, #chapters, 1 do
|
|
||||||
local end_chapter = chapters[j]
|
|
||||||
local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)')
|
|
||||||
if end_match then
|
|
||||||
local range = table_assign({
|
|
||||||
start_chapter = chapter, end_chapter = end_chapter,
|
|
||||||
start = chapter.time, ['end'] = end_chapter.time,
|
|
||||||
}, config.chapter_ranges.ads)
|
|
||||||
ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range
|
|
||||||
end_chapter.is_end_only = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end -- single chapter for ad
|
|
||||||
elseif not chapter.is_end_only and
|
|
||||||
(chapter.lowercase_title:find('%[sponsorblock%]:') or chapter.lowercase_title:find('^sponsors?')) then
|
|
||||||
local next_chapter = chapters[i + 1]
|
|
||||||
ranges[#ranges + 1] = table_assign({
|
|
||||||
start = chapter.time,
|
|
||||||
['end'] = next_chapter and next_chapter.time or INFINITY,
|
|
||||||
}, config.chapter_ranges.ads)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Fix overlapping sponsor block segments
|
|
||||||
for index, range in ipairs(sponsor_ranges) do
|
|
||||||
local next_range = sponsor_ranges[index + 1]
|
|
||||||
if next_range then
|
|
||||||
local delta = next_range.start - range['end']
|
|
||||||
if delta < 0 then
|
|
||||||
local mid_point = range['end'] + delta / 2
|
|
||||||
range['end'], range.end_chapter.time = mid_point - 0.01, mid_point - 0.01
|
|
||||||
next_range.start, next_range.start_chapter.time = mid_point, mid_point
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(chapters, function(a, b) return a.time < b.time end)
|
|
||||||
|
|
||||||
return chapters, ranges
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ensures chapters are in chronological order
|
|
||||||
function normalize_chapters(chapters)
|
|
||||||
if not chapters then return {} end
|
|
||||||
-- Ensure chronological order
|
|
||||||
table.sort(chapters, function(a, b) return a.time < b.time end)
|
|
||||||
-- Ensure titles
|
|
||||||
for index, chapter in ipairs(chapters) do
|
|
||||||
chapter.title = chapter.title or ('Chapter ' .. index)
|
|
||||||
chapter.lowercase_title = chapter.title:lower()
|
|
||||||
end
|
|
||||||
return chapters
|
|
||||||
end
|
|
||||||
|
|
||||||
function serialize_chapters(chapters)
|
|
||||||
chapters = normalize_chapters(chapters)
|
|
||||||
if not chapters then return end
|
|
||||||
--- timeline font size isn't accessible here, so normalize to size 1 and then scale during rendering
|
|
||||||
local opts = {size = 1, bold = true}
|
|
||||||
for index, chapter in ipairs(chapters) do
|
|
||||||
chapter.index = index
|
|
||||||
chapter.title_wrapped = wrap_text(chapter.title, opts, 25)
|
|
||||||
chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts)
|
|
||||||
chapter.title_wrapped = ass_escape(chapter.title_wrapped)
|
|
||||||
end
|
|
||||||
return chapters
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ RENDERING ]]
|
|
||||||
|
|
||||||
function render()
|
|
||||||
if not display.initialized then return end
|
|
||||||
state.render_last_time = mp.get_time()
|
|
||||||
|
|
||||||
cursor.reset_handlers()
|
|
||||||
|
|
||||||
-- Actual rendering
|
|
||||||
local ass = assdraw.ass_new()
|
|
||||||
|
|
||||||
for _, element in Elements:ipairs() do
|
|
||||||
if element.enabled then
|
|
||||||
local result = element:maybe('render')
|
|
||||||
if result then
|
|
||||||
ass:new_event()
|
|
||||||
ass:merge(result)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cursor.decide_keybinds()
|
|
||||||
|
|
||||||
-- submit
|
|
||||||
if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
osd.res_x = display.width
|
|
||||||
osd.res_y = display.height
|
|
||||||
osd.data = ass.text
|
|
||||||
osd.z = 2000
|
|
||||||
osd:update()
|
|
||||||
|
|
||||||
update_margins()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Request that render() is called.
|
|
||||||
-- The render is then either executed immediately, or rate-limited if it was
|
|
||||||
-- called a small time ago.
|
|
||||||
state.render_timer = mp.add_timeout(0, render)
|
|
||||||
state.render_timer:kill()
|
|
||||||
function request_render()
|
|
||||||
if state.render_timer:is_enabled() then return end
|
|
||||||
local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time))
|
|
||||||
state.render_timer.timeout = timeout
|
|
||||||
state.render_timer:resume()
|
|
||||||
end
|
|
|
@ -1,5 +0,0 @@
|
||||||
--[[
|
|
||||||
File required for compatibility between mpv:
|
|
||||||
- 0.32 - doesn't support `dir/main.lua`, so we need `uosc.lua` in root
|
|
||||||
- 0.33 - requires `main.lua` in directories
|
|
||||||
]]
|
|
|
@ -70,6 +70,10 @@ map('n', '<localleader>ln', '<cmd>lua vim.lsp.buf.rename()<cr>',
|
||||||
{ buffer = bufnr, desc = 'Rename element' })
|
{ buffer = bufnr, desc = 'Rename element' })
|
||||||
map('n', '<localleader>lr', '<cmd>lua vim.lsp.buf.references()<cr>',
|
map('n', '<localleader>lr', '<cmd>lua vim.lsp.buf.references()<cr>',
|
||||||
{ buffer = bufnr, desc = 'References' })
|
{ buffer = bufnr, desc = 'References' })
|
||||||
|
if client and client.server_capabilities.document_formatting then
|
||||||
|
map('n', '<localleader>lf', "<cmd>lua vim.lsp.buf.formatting()<CR>",
|
||||||
|
{ buffer = bufnr, desc = 'Format document' })
|
||||||
|
end
|
||||||
|
|
||||||
map('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>',
|
map('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>',
|
||||||
{ buffer = bufnr, desc = 'Hover definition' })
|
{ buffer = bufnr, desc = 'Hover definition' })
|
||||||
|
|
|
@ -7,69 +7,65 @@
|
||||||
"cmp-beancount": { "branch": "main", "commit": "da154ea94d598e6649d6ad01efa0a8611eff460d" },
|
"cmp-beancount": { "branch": "main", "commit": "da154ea94d598e6649d6ad01efa0a8611eff460d" },
|
||||||
"cmp-buffer": { "branch": "main", "commit": "3022dbc9166796b644a841a02de8dd1cc1d311fa" },
|
"cmp-buffer": { "branch": "main", "commit": "3022dbc9166796b644a841a02de8dd1cc1d311fa" },
|
||||||
"cmp-calc": { "branch": "main", "commit": "50792f34a628ea6eb31d2c90e8df174671e4e7a0" },
|
"cmp-calc": { "branch": "main", "commit": "50792f34a628ea6eb31d2c90e8df174671e4e7a0" },
|
||||||
"cmp-cmdline": { "branch": "main", "commit": "5af1bb7d722ef8a96658f01d6eb219c4cf746b32" },
|
"cmp-cmdline": { "branch": "main", "commit": "8fcc934a52af96120fe26358985c10c035984b53" },
|
||||||
"cmp-digraphs": { "branch": "master", "commit": "5efc1f0078d7c5f3ea1c8e3aad04da3fd6e081a9" },
|
"cmp-digraphs": { "branch": "master", "commit": "5efc1f0078d7c5f3ea1c8e3aad04da3fd6e081a9" },
|
||||||
"cmp-latex-symbols": { "branch": "main", "commit": "165fb66afdbd016eaa1570e41672c4c557b57124" },
|
"cmp-latex-symbols": { "branch": "main", "commit": "165fb66afdbd016eaa1570e41672c4c557b57124" },
|
||||||
"cmp-nvim-lsp": { "branch": "main", "commit": "0e6b2ed705ddcff9738ec4ea838141654f12eeef" },
|
"cmp-nvim-lsp": { "branch": "main", "commit": "0e6b2ed705ddcff9738ec4ea838141654f12eeef" },
|
||||||
"cmp-nvim-lua": { "branch": "main", "commit": "f12408bdb54c39c23e67cab726264c10db33ada8" },
|
"cmp-nvim-lua": { "branch": "main", "commit": "f3491638d123cfd2c8048aefaf66d246ff250ca6" },
|
||||||
"cmp-pandoc-references": { "branch": "master", "commit": "2c808dff631a783ddd2c554c4c6033907589baf6" },
|
"cmp-pandoc-references": { "branch": "master", "commit": "2c808dff631a783ddd2c554c4c6033907589baf6" },
|
||||||
"cmp-path": { "branch": "main", "commit": "91ff86cd9c29299a64f968ebb45846c485725f23" },
|
"cmp-path": { "branch": "main", "commit": "91ff86cd9c29299a64f968ebb45846c485725f23" },
|
||||||
"cmp-rg": { "branch": "master", "commit": "1cad8eb315643d0df13c37401c03d7986f891011" },
|
"cmp-rg": { "branch": "master", "commit": "1cad8eb315643d0df13c37401c03d7986f891011" },
|
||||||
"cmp-spell": { "branch": "master", "commit": "60584cb75e5e8bba5a0c9e4c3ab0791e0698bffa" },
|
"cmp-spell": { "branch": "master", "commit": "60584cb75e5e8bba5a0c9e4c3ab0791e0698bffa" },
|
||||||
"cmp-tmux": { "branch": "main", "commit": "984772716f66d8ee88535a6bf3f94c4b4e1301f5" },
|
"cmp-tmux": { "branch": "main", "commit": "984772716f66d8ee88535a6bf3f94c4b4e1301f5" },
|
||||||
"cmp-treesitter": { "branch": "master", "commit": "389eadd48c27aa6dc0e6b992644704f026802a2e" },
|
"cmp-treesitter": { "branch": "master", "commit": "b40178b780d547bcf131c684bc5fd41af17d05f2" },
|
||||||
"cmp_luasnip": { "branch": "master", "commit": "18095520391186d634a0045dacaa346291096566" },
|
"cmp_luasnip": { "branch": "master", "commit": "18095520391186d634a0045dacaa346291096566" },
|
||||||
"completion-vcard": { "branch": "master", "commit": "2220fd517a985ececed1adcf0e5be8f2815564c7" },
|
"completion-vcard": { "branch": "master", "commit": "2220fd517a985ececed1adcf0e5be8f2815564c7" },
|
||||||
"dial.nvim": { "branch": "master", "commit": "54b503f906bc9e5ab85288414840a1b86d40769f" },
|
"dial.nvim": { "branch": "master", "commit": "54b503f906bc9e5ab85288414840a1b86d40769f" },
|
||||||
"dressing.nvim": { "branch": "master", "commit": "66e4990240f92e31b0d5e4df6deb6bb0160ae832" },
|
"dressing.nvim": { "branch": "master", "commit": "5f44f829481640be0f96759c965ae22a3bcaf7ce" },
|
||||||
"easyread.nvim": { "branch": "main", "commit": "0b07e315a4cd7d700c4a794bdddbec79fdc2628b" },
|
"easyread.nvim": { "branch": "main", "commit": "73df5f4dc8fd38bef079b890b2a34412844c00b1" },
|
||||||
"fidget.nvim": { "branch": "main", "commit": "0ba1e16d07627532b6cae915cc992ecac249fb97" },
|
"fidget.nvim": { "branch": "main", "commit": "688b4fec4517650e29c3e63cfbb6e498b3112ba1" },
|
||||||
"formatter.nvim": { "branch": "master", "commit": "fa4f2729cc2909db599169f22d8e55632d4c8d59" },
|
"formatter.nvim": { "branch": "master", "commit": "ed949c13e1a942db29ababa35e8c7864ced90eb6" },
|
||||||
"friendly-snippets": { "branch": "main", "commit": "1d0dac346de7c6895ac72528df3276386c6b149b" },
|
"friendly-snippets": { "branch": "main", "commit": "25ddcd96540a2ce41d714bd7fea2e7f75fea8ead" },
|
||||||
"fwatch.nvim": { "branch": "main", "commit": "a691f7349dc66285cd75a1a698dd28bca45f2bf8" },
|
"fwatch.nvim": { "branch": "main", "commit": "a691f7349dc66285cd75a1a698dd28bca45f2bf8" },
|
||||||
"gitsigns.nvim": { "branch": "main", "commit": "bb808fc7376ed7bac0fbe8f47b83d4bf01738167" },
|
"gitsigns.nvim": { "branch": "main", "commit": "bb808fc7376ed7bac0fbe8f47b83d4bf01738167" },
|
||||||
"jupyter-kernel.nvim": { "branch": "main", "commit": "5b409598033884a3d819e2a3bcd1fe340bc8d783" },
|
"jupyter-kernel.nvim": { "branch": "main", "commit": "997dd7303f1e9cb210e511364725cfc4b6c4aa36" },
|
||||||
"lazy.nvim": { "branch": "main", "commit": "aba872ec78ffe7f7367764ab0fff6f0170421fde" },
|
"lazy.nvim": { "branch": "main", "commit": "887eb75591520a01548134c4623617b639289d0b" },
|
||||||
"lightspeed.nvim": { "branch": "main", "commit": "299eefa6a9e2d881f1194587c573dad619fdb96f" },
|
"lightspeed.nvim": { "branch": "main", "commit": "299eefa6a9e2d881f1194587c573dad619fdb96f" },
|
||||||
"lsp-format.nvim": { "branch": "master", "commit": "ca0df5c8544e51517209ea7b86ecc522c98d4f0a" },
|
"lsp-format.nvim": { "branch": "master", "commit": "ca0df5c8544e51517209ea7b86ecc522c98d4f0a" },
|
||||||
"lsp-zero.nvim": { "branch": "v2.x", "commit": "56a50ebe9b0f46ecfabca3f1613084c74fd45414" },
|
"lsp-zero.nvim": { "branch": "v2.x", "commit": "7cb74b241c9d99b5d44f2111696cd9306848b25b" },
|
||||||
"lsp_signature.nvim": { "branch": "master", "commit": "4665921ff8e30601c7c1328625b3abc1427a6143" },
|
"lsp_signature.nvim": { "branch": "master", "commit": "4665921ff8e30601c7c1328625b3abc1427a6143" },
|
||||||
"lualine.nvim": { "branch": "master", "commit": "05d78e9fd0cdfb4545974a5aa14b1be95a86e9c9" },
|
"lualine.nvim": { "branch": "master", "commit": "e99d733e0213ceb8f548ae6551b04ae32e590c80" },
|
||||||
"magma-nvim-goose": { "branch": "main", "commit": "5d916c39c1852e09fcd39eab174b8e5bbdb25f8f" },
|
"magma-nvim-goose": { "branch": "main", "commit": "5d916c39c1852e09fcd39eab174b8e5bbdb25f8f" },
|
||||||
"markdown-preview.nvim": { "branch": "master", "commit": "9becceee5740b7db6914da87358a183ad11b2049" },
|
"markdown-preview.nvim": { "branch": "master", "commit": "9becceee5740b7db6914da87358a183ad11b2049" },
|
||||||
"mason-lspconfig.nvim": { "branch": "main", "commit": "90a8bbf106b85b76951a34c542058ffa807de2b1" },
|
"mason-lspconfig.nvim": { "branch": "main", "commit": "2b811031febe5f743e07305738181ff367e1e452" },
|
||||||
"mason.nvim": { "branch": "main", "commit": "253961cfe9b0a63b2524088be294acd7522366e5" },
|
"mason.nvim": { "branch": "main", "commit": "a192887fd0c29275cf2acb4a83bcbdf63399f8df" },
|
||||||
"mini.nvim": { "branch": "main", "commit": "889be69623395ad183ae6f3c21c8efe006350226" },
|
"mini.nvim": { "branch": "main", "commit": "427751024313e2270ca723eb16af7b218c83a7fc" },
|
||||||
"nabla.nvim": { "branch": "master", "commit": "8c143ad2b3ab3b8ffbd51e238ccfcbd246452a7e" },
|
|
||||||
"neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" },
|
"neural": { "branch": "main", "commit": "155618730b87a67655bdde373ee27bfce8b07ac9" },
|
||||||
"nui.nvim": { "branch": "main", "commit": "698e75814cd7c56b0dd8af4936bcef2d13807f3c" },
|
"nui.nvim": { "branch": "main", "commit": "0dc148c6ec06577fcf06cbab3b7dac96d48ba6be" },
|
||||||
"nvim-base16": { "branch": "master", "commit": "4f3aa29f49b38edb6db1c52cea57e64ce3de2373" },
|
"nvim-base16": { "branch": "master", "commit": "22bad36cd64e85afb0c9d0e9b92106b5ea6dabc6" },
|
||||||
"nvim-cmp": { "branch": "main", "commit": "d153771162bd9795d9f7142df5c674b61066a585" },
|
"nvim-cmp": { "branch": "main", "commit": "777450fd0ae289463a14481673e26246b5e38bf2" },
|
||||||
"nvim-colorizer.lua": { "branch": "master", "commit": "dde3084106a70b9a79d48f426f6d6fec6fd203f7" },
|
"nvim-colorizer.lua": { "branch": "master", "commit": "36c610a9717cc9ec426a07c8e6bf3b3abcb139d6" },
|
||||||
"nvim-lspconfig": { "branch": "master", "commit": "df58d91c9351a9dc5be6cf8d54f49ab0d9a64e73" },
|
"nvim-lspconfig": { "branch": "master", "commit": "0f94c5fded29c0024254259f3d8a0284bfb507ea" },
|
||||||
"nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" },
|
"nvim-notify": { "branch": "master", "commit": "bdd647f61a05c9b8a57c83b78341a0690e9c29d7" },
|
||||||
"nvim-surround": { "branch": "main", "commit": "e6047128e57c1aff1566fb9f627521d2887fc77a" },
|
"nvim-surround": { "branch": "main", "commit": "b98862527c4727c171f8626e04d8ae88ef6cb736" },
|
||||||
"nvim-toggleterm.lua": { "branch": "main", "commit": "026dff5e2b504941cf172691561a67ea362596aa" },
|
"nvim-toggleterm.lua": { "branch": "main", "commit": "a5638b2206c3930a16a24e5c184dddd572f8cd34" },
|
||||||
"nvim-tree.lua": { "branch": "master", "commit": "89816ace70642e9d3db0dab3dc68918f8979ec31" },
|
"nvim-treesitter": { "branch": "master", "commit": "c38646edf2bdfac157ca619697ecad9ea87fd469" },
|
||||||
"nvim-treesitter": { "branch": "master", "commit": "cc360a9beb1b30d172438f640e2c3450358c4086" },
|
"nvim-treesitter-context": { "branch": "master", "commit": "88d1627285f7477883516ef60521601862dae7a1" },
|
||||||
"nvim-treesitter-context": { "branch": "master", "commit": "f24a86c32238867f24fbff49913db0068f8488d2" },
|
|
||||||
"nvim-treesitter-textsubjects": { "branch": "master", "commit": "b913508f503527ff540f7fe2dcf1bf1d1f259887" },
|
"nvim-treesitter-textsubjects": { "branch": "master", "commit": "b913508f503527ff540f7fe2dcf1bf1d1f259887" },
|
||||||
"nvim-ts-context-commentstring": { "branch": "main", "commit": "0bf8fbc2ca8f8cdb6efbd0a9e32740d7a991e4c3" },
|
"nvim-ts-context-commentstring": { "branch": "main", "commit": "729d83ecb990dc2b30272833c213cc6d49ed5214" },
|
||||||
"nvim-ts-rainbow2": { "branch": "master", "commit": "cee4601ff8aac73dee4afa1074814343bb5a0b80" },
|
"nvim-ts-rainbow2": { "branch": "master", "commit": "cee4601ff8aac73dee4afa1074814343bb5a0b80" },
|
||||||
"nvim-web-devicons": { "branch": "master", "commit": "986875b7364095d6535e28bd4aac3a9357e91bbe" },
|
"otter.nvim": { "branch": "main", "commit": "cfb548957aed403d9838febd7223595d47b32031" },
|
||||||
"otter.nvim": { "branch": "main", "commit": "4630e71b3e94552b7b33ddbfca061d92d0b466c2" },
|
"playground": { "branch": "master", "commit": "4044b53c4d4fcd7a78eae20b8627f78ce7dc6f56" },
|
||||||
"playground": { "branch": "master", "commit": "2b81a018a49f8e476341dfcb228b7b808baba68b" },
|
|
||||||
"plenary.nvim": { "branch": "master", "commit": "253d34830709d690f013daf2853a9d21ad7accab" },
|
"plenary.nvim": { "branch": "master", "commit": "253d34830709d690f013daf2853a9d21ad7accab" },
|
||||||
"popup.nvim": { "branch": "master", "commit": "b7404d35d5d3548a82149238289fa71f7f6de4ac" },
|
"popup.nvim": { "branch": "master", "commit": "b7404d35d5d3548a82149238289fa71f7f6de4ac" },
|
||||||
"quarto-nvim": { "branch": "main", "commit": "43898e09b5f49dee35ff01ff0f873e7d600376be" },
|
"quarto-nvim": { "branch": "main", "commit": "91c82b96660d0b2d830c668365719b295272432d" },
|
||||||
"significant.nvim": { "branch": "main", "commit": "5450e9d5917dc6aa9afb0fcbe32355799b8303fb" },
|
"significant.nvim": { "branch": "main", "commit": "5450e9d5917dc6aa9afb0fcbe32355799b8303fb" },
|
||||||
"smartcolumn.nvim": { "branch": "main", "commit": "0c572e3eae48874f25b74394a486f38cadb5c958" },
|
|
||||||
"spellsitter.nvim": { "branch": "master", "commit": "4af8640d9d706447e78c13150ef7475ea2c16b30" },
|
"spellsitter.nvim": { "branch": "master", "commit": "4af8640d9d706447e78c13150ef7475ea2c16b30" },
|
||||||
"symbols-outline.nvim": { "branch": "master", "commit": "512791925d57a61c545bc303356e8a8f7869763c" },
|
"symbols-outline.nvim": { "branch": "master", "commit": "512791925d57a61c545bc303356e8a8f7869763c" },
|
||||||
"telescope-fzf-native.nvim": { "branch": "main", "commit": "580b6c48651cabb63455e97d7e131ed557b8c7e2" },
|
"telescope-fzf-native.nvim": { "branch": "main", "commit": "580b6c48651cabb63455e97d7e131ed557b8c7e2" },
|
||||||
"telescope.nvim": { "branch": "master", "commit": "c1a2af0af69e80e14e6b226d3957a064cd080805" },
|
"telescope.nvim": { "branch": "master", "commit": "c1a2af0af69e80e14e6b226d3957a064cd080805" },
|
||||||
"twilight.nvim": { "branch": "main", "commit": "8bb7fa7b918baab1ca81b977102ddb54afa63512" },
|
"twilight.nvim": { "branch": "main", "commit": "8bb7fa7b918baab1ca81b977102ddb54afa63512" },
|
||||||
"vifm.vim": { "branch": "master", "commit": "a8130c37d144b51d84bee19f0532abcd3583383f" },
|
"vifm.vim": { "branch": "master", "commit": "6898b7fcbc36324c127ba42cabe488ab15c785f4" },
|
||||||
"vim-criticmarkup": { "branch": "master", "commit": "d15dc134eb177a170c79f6377f81eb02a9d20b02" },
|
"vim-criticmarkup": { "branch": "master", "commit": "d15dc134eb177a170c79f6377f81eb02a9d20b02" },
|
||||||
"vim-easy-align": { "branch": "master", "commit": "0db4ea6132110631ec678a99a82aa49a0686ae65" },
|
"vim-easy-align": { "branch": "master", "commit": "0db4ea6132110631ec678a99a82aa49a0686ae65" },
|
||||||
"vim-exchange": { "branch": "master", "commit": "784d63083ad7d613aa96f00021cd0dfb126a781a" },
|
"vim-exchange": { "branch": "master", "commit": "784d63083ad7d613aa96f00021cd0dfb126a781a" },
|
||||||
|
@ -77,9 +73,9 @@
|
||||||
"vim-oscyank": { "branch": "main", "commit": "ffe827a27dae98aa826e2295336c650c9a434da0" },
|
"vim-oscyank": { "branch": "main", "commit": "ffe827a27dae98aa826e2295336c650c9a434da0" },
|
||||||
"vim-pandoc-syntax": { "branch": "master", "commit": "4268535e1d33117a680a91160d845cd3833dfe28" },
|
"vim-pandoc-syntax": { "branch": "master", "commit": "4268535e1d33117a680a91160d845cd3833dfe28" },
|
||||||
"vim-spellsync": { "branch": "master", "commit": "3d6dd50de9c4d953cc16638112a6ae196df41463" },
|
"vim-spellsync": { "branch": "master", "commit": "3d6dd50de9c4d953cc16638112a6ae196df41463" },
|
||||||
"which-key.nvim": { "branch": "main", "commit": "912ef1a9b018bbe45df1529345e42ae0ac896d63" },
|
"which-key.nvim": { "branch": "main", "commit": "2a0c2d80c0a60f041afb1b789cfedbd510e2b2b6" },
|
||||||
"wrapping.nvim": { "branch": "master", "commit": "c04a7163dc692d80a2907d06a3af8df1fedffec2" },
|
"wrapping.nvim": { "branch": "master", "commit": "a4013c377e2ffa3be00fb67791d3605ae3115acb" },
|
||||||
"zen-mode.nvim": { "branch": "main", "commit": "6e6c963d70a8e47854fa656987666bfb863f9c4e" },
|
"zen-mode.nvim": { "branch": "main", "commit": "d907e638c879642d226d27469b53db6925f69d4c" },
|
||||||
"zettelkasten.nvim": { "branch": "main", "commit": "0e77624689b470410f5355b613d45219c9350264" },
|
"zettelkasten.nvim": { "branch": "main", "commit": "0e77624689b470410f5355b613d45219c9350264" },
|
||||||
"zk-nvim": { "branch": "main", "commit": "275578853dc76d282ee5b31f86cd3a4f02d91f2f" }
|
"zk-nvim": { "branch": "main", "commit": "0413c52500cd0133b0cd8e7e7d43084855ac1760" }
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ local prefix = require('which-key').register
|
||||||
-- The general ideas behind these mappings:
|
-- The general ideas behind these mappings:
|
||||||
--
|
--
|
||||||
-- * Leader prefix is the generally preferred way to map new things, however
|
-- * Leader prefix is the generally preferred way to map new things, however
|
||||||
-- only for those that affect all of vim or work in a supra-buffer way.
|
-- only for those that affect all of vim of work in a supra-buffer way.
|
||||||
--
|
--
|
||||||
-- * Localleader prefix is used for mappings which only affect single buffers.
|
-- * Localleader prefix is used for mappings which only affect single buffers.
|
||||||
-- In other words mostly filetype specific mappings
|
-- In other words mostly filetype specific mappings
|
||||||
|
@ -211,10 +211,6 @@ map('n', '<leader>ss', ":lua MiniStarter.open()<cr>", { desc = 'show startpage'
|
||||||
map('n', '<leader>so', '<cmd>SymbolsOutline<cr>',
|
map('n', '<leader>so', '<cmd>SymbolsOutline<cr>',
|
||||||
{ silent = true, desc = 'toggle symbol outline' })
|
{ silent = true, desc = 'toggle symbol outline' })
|
||||||
|
|
||||||
-- PLUGIN: nvim-tree
|
|
||||||
map('n', '<leader>se', '<cmd>NvimTreeToggle<cr>',
|
|
||||||
{ silent = true, desc = 'toggle filetree' })
|
|
||||||
|
|
||||||
-- PLUGIN: easy-align
|
-- PLUGIN: easy-align
|
||||||
-- Start interactive EasyAlign in visual mode (e.g. vipga)
|
-- Start interactive EasyAlign in visual mode (e.g. vipga)
|
||||||
map('x', 'ga', '<Plug>(EasyAlign)')
|
map('x', 'ga', '<Plug>(EasyAlign)')
|
||||||
|
@ -236,48 +232,17 @@ map("v", "g<C-x>", 'g<Plug>(dial-increment)')
|
||||||
-- PLUGIN: zettelkasten.nvim
|
-- PLUGIN: zettelkasten.nvim
|
||||||
map('n', '<cr>', [[:silent lua require 'zettelkasten'.link_follow()<cr>]])
|
map('n', '<cr>', [[:silent lua require 'zettelkasten'.link_follow()<cr>]])
|
||||||
map('v', '<cr>', [[:lua require 'zettelkasten'.link_follow(true)<cr>]])
|
map('v', '<cr>', [[:lua require 'zettelkasten'.link_follow(true)<cr>]])
|
||||||
prefix({ ['<leader>n'] = { name = '+notes' } })
|
prefix({ ['<leader>w'] = { name = '+wiki' } })
|
||||||
map('n', '<leader>ni', [[:lua require 'zettelkasten'.index_open()<cr> ]],
|
map('n', '<leader>ww', [[:lua require 'zettelkasten'.index_open()<cr> ]],
|
||||||
{ desc = "index page" })
|
{ desc = "open wiki" })
|
||||||
-- PLUGIN: zk
|
|
||||||
map('n', '<leader>nn', "<cmd>ZkNotes { sort = { 'modified' } }<cr>",
|
|
||||||
{ desc = "note list" })
|
|
||||||
map("n", "<leader>nf", "<Cmd>ZkNotes { sort = { 'modified' }, match = { vim.fn.input('Search: ') } }<CR>",
|
|
||||||
{ desc = "note search" })
|
|
||||||
map('n', '<leader>nt', "<cmd>ZkTags<cr>",
|
|
||||||
{ desc = "note tags" })
|
|
||||||
map('n', '<leader>nc', "<cmd>ZkCd<cr>",
|
|
||||||
{ desc = "notes directory" })
|
|
||||||
prefix({ ['<localleader>n'] = { name = '+note' } })
|
|
||||||
map('n', '<localleader>nl', "<cmd>ZkLinks<cr>",
|
|
||||||
{ desc = "note links" })
|
|
||||||
map('n', '<localleader>nb', "<cmd>ZkLinks<cr>",
|
|
||||||
{ desc = "note backlinks" })
|
|
||||||
map('n', '<localleader>nn', "<cmd>ZkNew { title = vim.fn.input('Title: ') }<cr>",
|
|
||||||
{ desc = "new note" })
|
|
||||||
prefix({ ['<localleader>n'] = { name = '+note', mode = "v" } })
|
|
||||||
map('v', '<localleader>nn', ":ZkNewFromTitleSelection<cr>",
|
|
||||||
{ desc = "title from selection" })
|
|
||||||
map('v', '<localleader>nN', ":ZkNewFromContentSelection<cr>",
|
|
||||||
{ desc = "content from selection" })
|
|
||||||
map('v', '<localleader>nf', ":ZkMatch<cr>",
|
|
||||||
{ desc = "find note from selection" })
|
|
||||||
|
|
||||||
-- PLUGIN: toggleterm.nvim
|
-- PLUGIN: toggleterm.nvim
|
||||||
-- create a lazygit window, set up in toggleterm settings
|
-- create a lazygit window, set up in toggleterm settings
|
||||||
map('n', '<leader>G', ':Lazygit<cr>')
|
map('n', '<leader>G', ':Lazygit<cr>')
|
||||||
|
|
||||||
prefix({ ['<localleader>s'] = { name = '+set' } })
|
|
||||||
-- PLUGIN: wrapping.nvim
|
-- PLUGIN: wrapping.nvim
|
||||||
map('n', '<localleader>sw', [[:lua require('wrapping').toggle_wrap_mode()<cr> ]],
|
map('n', '<localleader>sw', [[:lua require('wrapping').toggle_wrap_mode()<cr> ]],
|
||||||
{ silent = true, desc = 'toggle wrap mode' })
|
{ silent = true, desc = 'toggle wrap mode' })
|
||||||
|
|
||||||
-- PLUGIN: easyread.nvim
|
-- PLUGIN: easyread.nvim
|
||||||
map('n', '<localleader>ss', ':EasyreadToggle<cr>', { silent = true, desc = 'toggle speedreading' })
|
map('n', '<localleader>ss', ':EasyreadToggle<cr>', { silent = true, desc = 'toggle speedreading' })
|
||||||
-- PLUGIN: nabla.nvim
|
|
||||||
map('n', '<localleader>sv', '<cmd>lua require("nabla").popup()<cr>', { silent = true, desc = 'latex formula popup' })
|
|
||||||
map('n', '<localleader>sV', '<cmd>lua require("nabla").toggle_virt({autogen = true, silent = true})<cr>',
|
|
||||||
{ silent = true, desc = 'toggle formula notation' })
|
|
||||||
-- PLUGIN: nvim-colorizer
|
|
||||||
map('n', '<localleader>sc', '<cmd>ColorizerToggle<cr>', { silent = true, desc = 'toggle colorizer' })
|
|
||||||
map('n', '<localleader>sC', '<cmd>lua require("colorizer").attach_to_buffer(0, {mode = "background"} )<cr>',
|
|
||||||
{ silent = true, desc = 'colorize background' })
|
|
||||||
|
|
|
@ -1,44 +1,40 @@
|
||||||
require('gitsigns').setup {
|
-- require('gitsigns').setup {
|
||||||
numhl = true,
|
-- on_attach = function(bufnr)
|
||||||
signcolumn = false,
|
-- local gs = package.loaded.gitsigns
|
||||||
on_attach = function(bufnr)
|
--
|
||||||
local gs = package.loaded.gitsigns
|
-- local function map(mode, l, r, opts)
|
||||||
|
-- opts = opts or {}
|
||||||
local function map(mode, l, r, opts)
|
-- opts.buffer = bufnr
|
||||||
opts = opts or {}
|
-- vim.keymap.set(mode, l, r, opts)
|
||||||
opts.buffer = bufnr
|
-- end
|
||||||
vim.keymap.set(mode, l, r, opts)
|
--
|
||||||
end
|
-- -- Navigation
|
||||||
|
-- map('n', ']h', function()
|
||||||
-- Navigation
|
-- if vim.wo.diff then return ']h' end
|
||||||
map('n', ']h', function()
|
-- vim.schedule(function() gs.next_hunk() end)
|
||||||
if vim.wo.diff then return ']h' end
|
-- return '<Ignore>'
|
||||||
vim.schedule(function() gs.next_hunk() end)
|
-- end, {expr = true})
|
||||||
return '<Ignore>'
|
--
|
||||||
end, { expr = true })
|
-- map('n', '[h', function()
|
||||||
|
-- if vim.wo.diff then return '[h' end
|
||||||
map('n', '[h', function()
|
-- vim.schedule(function() gs.prev_hunk() end)
|
||||||
if vim.wo.diff then return '[h' end
|
-- return '<Ignore>'
|
||||||
vim.schedule(function() gs.prev_hunk() end)
|
-- end, {expr = true})
|
||||||
return '<Ignore>'
|
--
|
||||||
end, { expr = true })
|
-- -- Actions
|
||||||
|
-- map({'n', 'v'}, '<leader>hs', ':Gitsigns stage_hunk<CR>')
|
||||||
-- Actions
|
-- map('n', '<leader>hS', gs.stage_buffer)
|
||||||
require('which-key').register({ ['<localleader>h'] = { name = '+git' } })
|
-- map({'n', 'v'}, '<leader>hr', ':Gitsigns reset_hunk<CR>')
|
||||||
map({ 'n', 'v' }, '<localleader>hs', ':Gitsigns stage_hunk<CR>', { desc = 'stage hunk' })
|
-- map('n', '<leader>hR', gs.reset_buffer)
|
||||||
map({ 'n', 'v' }, '<localleader>hr', ':Gitsigns reset_hunk<CR>', { desc = 'reset hunk' })
|
-- map('n', '<leader>hu', gs.undo_stage_hunk)
|
||||||
map('n', '<localleader>hS', gs.stage_buffer, { desc = 'stage buffer' })
|
-- map('n', '<leader>hp', gs.preview_hunk)
|
||||||
map('n', '<localleader>hu', gs.undo_stage_hunk, { desc = 'undo stage hunk' })
|
-- map('n', '<leader>hb', function() gs.blame_line {full = true} end)
|
||||||
map('n', '<localleader>hR', gs.reset_buffer, { desc = 'reset buffer' })
|
-- map('n', '<leader>hB', gs.toggle_current_line_blame)
|
||||||
map('n', '<localleader>hp', gs.preview_hunk, { desc = 'preview hunk' })
|
-- map('n', '<leader>hd', gs.diffthis)
|
||||||
map('n', '<localleader>hb', function() gs.blame_line { full = true } end, { desc = 'blame line' })
|
-- map('n', '<leader>hD', function() gs.diffthis('~') end)
|
||||||
map('n', '<localleader>hB', gs.toggle_current_line_blame, { desc = 'toggle blame' })
|
-- map('n', '<leader>hdd', gs.toggle_deleted)
|
||||||
map('n', '<localleader>hd', gs.diffthis, { desc = 'diffthis' })
|
--
|
||||||
map('n', '<localleader>hD', function() gs.diffthis('~') end, { desc = 'diffbase' })
|
-- -- Text object
|
||||||
map('n', '<localleader>ht', gs.toggle_deleted, { desc = 'toggle deleted' })
|
-- map({'o', 'x'}, 'ih', ':<C-U>Gitsigns select_hunk<CR>')
|
||||||
|
-- end
|
||||||
-- Text object
|
-- }
|
||||||
map({ 'o', 'x' }, 'ih', ':<C-U>Gitsigns select_hunk<CR>')
|
|
||||||
map({ 'o', 'x' }, 'ah', ':<C-U>Gitsigns select_hunk<CR>')
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,17 +7,9 @@ vim.fn.sign_define("DiagnosticSignInfo", { text = "", texthl = "DiagnosticSig
|
||||||
vim.fn.sign_define("DiagnosticSignHint", { text = "", texthl = "DiagnosticSignHint" })
|
vim.fn.sign_define("DiagnosticSignHint", { text = "", texthl = "DiagnosticSignHint" })
|
||||||
|
|
||||||
lsp.ensure_installed({
|
lsp.ensure_installed({
|
||||||
'arduino_language_server',
|
'arduino_language_server', 'bashls', 'beancount', 'clangd', 'dockerls',
|
||||||
'bashls',
|
'docker_compose_language_service', 'lua_ls', 'pyright', 'ruff_lsp', 'taplo',
|
||||||
'beancount',
|
'yamlls'
|
||||||
'clangd',
|
|
||||||
'dockerls',
|
|
||||||
'docker_compose_language_service',
|
|
||||||
'lua_ls',
|
|
||||||
'pyright',
|
|
||||||
'ruff_lsp',
|
|
||||||
'taplo',
|
|
||||||
'yamlls',
|
|
||||||
})
|
})
|
||||||
lsp.preset({ name = "recommended", set_lsp_keymaps = false })
|
lsp.preset({ name = "recommended", set_lsp_keymaps = false })
|
||||||
lsp.on_attach(function(client, bufnr)
|
lsp.on_attach(function(client, bufnr)
|
||||||
|
@ -157,7 +149,7 @@ cmp.setup({
|
||||||
{ name = 'latex_symbols' },
|
{ name = 'latex_symbols' },
|
||||||
{ name = 'spell', keyword_length = 3 },
|
{ name = 'spell', keyword_length = 3 },
|
||||||
{ name = 'tmux' },
|
{ name = 'tmux' },
|
||||||
--{ name = 'rg', keyword_length = 5 },
|
{ name = 'rg', keyword_length = 5 },
|
||||||
{ name = 'vCard' },
|
{ name = 'vCard' },
|
||||||
},
|
},
|
||||||
mapping = cmp.mapping.preset.insert({
|
mapping = cmp.mapping.preset.insert({
|
||||||
|
|
|
@ -34,5 +34,3 @@ starter.setup({
|
||||||
starter.gen_hook.aligning('center', 'center')
|
starter.gen_hook.aligning('center', 'center')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.api.nvim_set_hl(0, 'MiniCursorword', { bold = true })
|
|
||||||
|
|
|
@ -9,8 +9,10 @@ local lazygit = Terminal:new({
|
||||||
cmd = "lazygit",
|
cmd = "lazygit",
|
||||||
hidden = true,
|
hidden = true,
|
||||||
direction = 'float',
|
direction = 'float',
|
||||||
float_opts = { border = "curved" }
|
float_opts = {border = "curved"}
|
||||||
})
|
})
|
||||||
function _Lazygit_toggle() lazygit:toggle() end
|
function _Lazygit_toggle() lazygit:toggle() end
|
||||||
|
|
||||||
vim.cmd([[command! Lazygit :lua _Lazygit_toggle()<cr>]])
|
vim.cmd([[command! Lazygit :lua _Lazygit_toggle()<cr>]])
|
||||||
|
|
||||||
|
-- vim.api.nvim_set_keymap("n", "<leader>g", "<cmd>lua _lazygit_toggle()<CR>",
|
||||||
|
-- {noremap = true, silent = true})
|
||||||
|
|
|
@ -1,39 +1,25 @@
|
||||||
local writing_ft = { "quarto", "pandoc", "markdown", "text", "tex" }
|
local writing_ft = { "quarto", "pandoc", "markdown", "text", "tex" }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
-- vim plugs
|
||||||
-- essential
|
-- essential
|
||||||
{ 'numToStr/Navigator.nvim', branch = "master", config = true }, -- allow seamless navigation between vim buffers and tmux/wezterm splits
|
{ 'numToStr/Navigator.nvim', branch = "master", config = true }, -- allow seamless navigation between vim buffers and tmux/wezterm splits
|
||||||
{ 'jeffkreeftmeijer/vim-numbertoggle', event = "BufEnter" }, -- toggles numbers to absolute for all buffers but the current which is relative
|
{ 'jeffkreeftmeijer/vim-numbertoggle', event = "BufEnter" }, -- toggles numbers to absolute for all buffers but the current which is relative
|
||||||
{ 'ojroques/vim-oscyank', event = "VeryLazy" }, -- yank from *anywhere* (even ssh session) to clipboard, using :OSCYank
|
{ 'ojroques/vim-oscyank', event = "VeryLazy" }, -- yank from *anywhere* (even ssh session) to clipboard, using :OSCYank
|
||||||
{ 'ggandor/lightspeed.nvim', event = "VeryLazy" }, -- jump between letters with improved fFtT quicksearch, mimics sneak
|
{ 'ggandor/lightspeed.nvim', event = "VeryLazy" }, -- jump between letters with improved fFtT quicksearch, mimics sneak
|
||||||
{
|
|
||||||
'lewis6991/gitsigns.nvim', -- show vcs changes on left-hand gutter
|
|
||||||
config = function()
|
|
||||||
require('plug._gitsigns')
|
|
||||||
end,
|
|
||||||
event = "BufRead"
|
|
||||||
}, { "m4xshen/smartcolumn.nvim", config = true }, -- auto-hiding colorcolumn
|
|
||||||
-- files
|
-- files
|
||||||
{ 'vifm/vifm.vim' }, -- integrate file manager
|
{ 'vifm/vifm.vim' }, -- integrate file manager
|
||||||
{
|
{
|
||||||
'nvim-tree/nvim-tree.lua', -- integrate file tree
|
'lewis6991/gitsigns.nvim', -- show vcs changes on left-hand gutter
|
||||||
config = true,
|
opts = { numhl = true, signcolumn = false },
|
||||||
dependencies = { 'nvim-tree/nvim-web-devicons', config = true },
|
event = "BufRead"
|
||||||
cmd = "NvimTreeToggle"
|
}, {
|
||||||
}, -- colors
|
|
||||||
{
|
|
||||||
'RRethy/nvim-base16',
|
'RRethy/nvim-base16',
|
||||||
event = "BufWinEnter",
|
event = "BufWinEnter",
|
||||||
dependencies = { 'rktjmp/fwatch.nvim' }
|
dependencies = { 'rktjmp/fwatch.nvim' }
|
||||||
}, {
|
}, {
|
||||||
'NvChad/nvim-colorizer.lua', -- color hex, named colors in the correct preview scheme
|
'norcalli/nvim-colorizer.lua', -- color hex, named colors in the correct preview scheme
|
||||||
config = function()
|
config = true,
|
||||||
require('colorizer').setup({
|
|
||||||
user_default_options = {
|
|
||||||
mode = 'virtualtext'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
event = "VeryLazy"
|
event = "VeryLazy"
|
||||||
}, {
|
}, {
|
||||||
'mhartington/formatter.nvim', -- auto formatting on save
|
'mhartington/formatter.nvim', -- auto formatting on save
|
||||||
|
@ -80,16 +66,10 @@ return {
|
||||||
}, { 'edKotinsky/Arduino.nvim', ft = 'arduino', config = true }, -- statusline
|
}, { 'edKotinsky/Arduino.nvim', ft = 'arduino', config = true }, -- statusline
|
||||||
{
|
{
|
||||||
'nvim-lualine/lualine.nvim',
|
'nvim-lualine/lualine.nvim',
|
||||||
requires = { 'nvim-tree/nvim-web-devicons', config = true },
|
requires = { 'kyazdani42/nvim-web-devicons', opt = true },
|
||||||
config = function() require('plug._lualine') end
|
config = function() require('plug._lualine') end
|
||||||
}, -- writing
|
}, -- writing
|
||||||
{ 'vim-pandoc/vim-criticmarkup', ft = writing_ft }, {
|
{ 'vim-pandoc/vim-criticmarkup', ft = writing_ft }, {
|
||||||
'jbyuki/nabla.nvim',
|
|
||||||
ft = writing_ft,
|
|
||||||
config = function()
|
|
||||||
require('nabla').enable_virt({ autogen = true, silent = true })
|
|
||||||
end
|
|
||||||
}, {
|
|
||||||
'mickael-menu/zk-nvim',
|
'mickael-menu/zk-nvim',
|
||||||
config = function() require('zk').setup({ picker = "telescope" }) end
|
config = function() require('zk').setup({ picker = "telescope" }) end
|
||||||
}, {
|
}, {
|
||||||
|
@ -156,11 +136,12 @@ return {
|
||||||
vim.g.magma_image_provider = "kitty"
|
vim.g.magma_image_provider = "kitty"
|
||||||
vim.g.magma_automatically_open_output = false
|
vim.g.magma_automatically_open_output = false
|
||||||
end
|
end
|
||||||
}, {
|
}, -- nvim plugs
|
||||||
|
{
|
||||||
'echasnovski/mini.nvim',
|
'echasnovski/mini.nvim',
|
||||||
version = '*',
|
version = '*',
|
||||||
config = function() require('plug._mini') end
|
config = function() require('plug._mini') end
|
||||||
}, {
|
}, {
|
||||||
"akinsho/nvim-toggleterm.lua", -- simpler, programmable and multiple terminal toggling for nvim
|
"akinsho/nvim-toggleterm.lua", -- simpler, programmable and multiple terminal toggling for nvim
|
||||||
config = function() require('plug._toggleterm') end
|
config = function() require('plug._toggleterm') end
|
||||||
},
|
},
|
||||||
|
@ -219,21 +200,32 @@ return {
|
||||||
"VonHeikemen/lsp-zero.nvim",
|
"VonHeikemen/lsp-zero.nvim",
|
||||||
dependencies = {
|
dependencies = {
|
||||||
{ "neovim/nvim-lspconfig", branch = "master" },
|
{ "neovim/nvim-lspconfig", branch = "master" },
|
||||||
"williamboman/mason.nvim", "williamboman/mason-lspconfig.nvim", {
|
"williamboman/mason.nvim",
|
||||||
|
"williamboman/mason-lspconfig.nvim",
|
||||||
|
{
|
||||||
"hrsh7th/nvim-cmp",
|
"hrsh7th/nvim-cmp",
|
||||||
branch = "main",
|
branch = "main",
|
||||||
dependencies = {
|
dependencies = {
|
||||||
"andersevenrud/cmp-tmux", "cbarrete/completion-vcard",
|
"andersevenrud/cmp-tmux",
|
||||||
"f3fora/cmp-spell", "hrsh7th/cmp-nvim-lsp",
|
"cbarrete/completion-vcard",
|
||||||
"hrsh7th/cmp-path", "hrsh7th/cmp-buffer",
|
"f3fora/cmp-spell",
|
||||||
"hrsh7th/cmp-calc", "hrsh7th/cmp-cmdline",
|
"hrsh7th/cmp-nvim-lsp",
|
||||||
"hrsh7th/cmp-nvim-lua", "dmitmel/cmp-digraphs",
|
"hrsh7th/cmp-path",
|
||||||
|
"hrsh7th/cmp-buffer",
|
||||||
|
"hrsh7th/cmp-calc",
|
||||||
|
"hrsh7th/cmp-cmdline",
|
||||||
|
"hrsh7th/cmp-nvim-lua",
|
||||||
|
"dmitmel/cmp-digraphs",
|
||||||
"jc-doyle/cmp-pandoc-references",
|
"jc-doyle/cmp-pandoc-references",
|
||||||
"kdheepak/cmp-latex-symbols", "lukas-reineke/cmp-rg",
|
"kdheepak/cmp-latex-symbols",
|
||||||
"crispgm/cmp-beancount", "ray-x/cmp-treesitter",
|
"lukas-reineke/cmp-rg",
|
||||||
"saadparwaiz1/cmp_luasnip"
|
"crispgm/cmp-beancount",
|
||||||
|
"ray-x/cmp-treesitter",
|
||||||
|
"saadparwaiz1/cmp_luasnip",
|
||||||
}
|
}
|
||||||
}, "L3MON4D3/LuaSnip", "rafamadriz/friendly-snippets",
|
},
|
||||||
|
"L3MON4D3/LuaSnip",
|
||||||
|
"rafamadriz/friendly-snippets",
|
||||||
{ "lukas-reineke/lsp-format.nvim", config = true },
|
{ "lukas-reineke/lsp-format.nvim", config = true },
|
||||||
{ "j-hui/fidget.nvim", config = true } -- loading animations for some LSP
|
{ "j-hui/fidget.nvim", config = true } -- loading animations for some LSP
|
||||||
},
|
},
|
||||||
|
|
|
@ -180,4 +180,3 @@ reproducability
|
||||||
positivity
|
positivity
|
||||||
dataset
|
dataset
|
||||||
endogeneity
|
endogeneity
|
||||||
outliers
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ color progress black cyan
|
||||||
|
|
||||||
# Formatting ----------------------------------------------------------------------
|
# Formatting ----------------------------------------------------------------------
|
||||||
set date_format = "%a %d %h %H:%M"
|
set date_format = "%a %d %h %H:%M"
|
||||||
set index_format=" %zc %zs %zt | %-35.35L %?X?📎& ? %?M10?~(%1M) ?%-30.100s %> %?Y?%Y ? %(!%a %d %h %H:%M) "
|
set index_format=" %zc %zs %zt | %-35.35L %@attachment_info@ %?M10?~(%1M) ?%-30.100s %> %?Y?%Y ? %(!%a %d %h %H:%M) "
|
||||||
set pager_format="%n %T %s%* %{!%d %b · %H:%M} %?X? %X?%P"
|
set pager_format="%n %T %s%* %{!%d %b · %H:%M} %?X? %X?%P"
|
||||||
set status_format = " %D %?u? %u ?%?R? %R ?%?d? %d ?%?t? %t ?%?F? %F ?%?p? %p? \n \n"
|
set status_format = " %D %?u? %u ?%?R? %R ?%?d? %d ?%?t? %t ?%?F? %F ?%?p? %p? \n \n"
|
||||||
set compose_format="-- NeoMutt: Compose [Approx. msg size: %l Atts: %a]%>-"
|
set compose_format="-- NeoMutt: Compose [Approx. msg size: %l Atts: %a]%>-"
|
||||||
|
|
|
@ -21,9 +21,9 @@ bind index,pager w display-toggle-weed
|
||||||
|
|
||||||
# Thread manipulation
|
# Thread manipulation
|
||||||
bind pager d noop
|
bind pager d noop
|
||||||
bind index,pager dD delete-message
|
bind index,pager dd delete-message
|
||||||
bind index,pager dd delete-subthread
|
bind index,pager dT delete-thread
|
||||||
bind index,pager dt delete-thread
|
bind index,pager dt delete-subthread
|
||||||
bind pager,index gt next-thread
|
bind pager,index gt next-thread
|
||||||
bind pager,index gT previous-thread
|
bind pager,index gT previous-thread
|
||||||
bind pager,index za collapse-thread
|
bind pager,index za collapse-thread
|
||||||
|
@ -31,15 +31,15 @@ bind pager,index zA collapse-all
|
||||||
bind pager,index zr reconstruct-thread
|
bind pager,index zr reconstruct-thread
|
||||||
bind pager,index zR entire-thread
|
bind pager,index zR entire-thread
|
||||||
# Saner copy/move dialogs
|
# Saner copy/move dialogs
|
||||||
macro index,pager ,c "<copy-message>?<toggle-mailboxes>" "copy a message to a mailbox"
|
macro index,pager C "<copy-message>?<toggle-mailboxes>" "copy a message to a mailbox"
|
||||||
macro index,pager ,m "<tag-thread><tag-prefix><save-message>?<toggle-mailboxes>" "move thread to a mailbox"
|
macro index,pager M "<save-message>?<toggle-mailboxes>" "move a message to a mailbox"
|
||||||
macro index,pager ,M "<save-message>?<toggle-mailboxes>" "move a message to a mailbox"
|
macro index,pager gM "<tag-thread><tag-prefix><save-message>?<toggle-mailboxes>" "move thread to a mailbox"
|
||||||
|
|
||||||
# Email completion bindings
|
# Email completion bindings
|
||||||
bind editor <Tab> complete-query
|
bind editor <Tab> complete-query
|
||||||
bind editor ^T complete
|
bind editor ^T complete
|
||||||
# Press A to add contact to Khard address book
|
# Press A to add contact to Khard address book
|
||||||
macro index,pager ,a \
|
macro index,pager A \
|
||||||
"<pipe-message>khard add-email<return>" \
|
"<pipe-message>khard add-email<return>" \
|
||||||
"add the sender email address to khard"
|
"add the sender email address to khard"
|
||||||
|
|
||||||
|
@ -69,14 +69,14 @@ bind pager G bottom
|
||||||
# compose postpone
|
# compose postpone
|
||||||
bind compose p postpone-message
|
bind compose p postpone-message
|
||||||
# markdown to html for composition
|
# markdown to html for composition
|
||||||
macro compose ,m "F pandoc -s -f markdown -t html \ny^T^Utext/html; charset=UTF-8\n" "Convert from MD to HTML"
|
macro compose M "F pandoc -s -f markdown -t html \ny^T^Utext/html; charset=UTF-8\n" "Convert from MD to HTML"
|
||||||
|
|
||||||
# since we unbound the original g
|
# since we unbound the original g
|
||||||
bind index,pager r noop # to avoid accidentally sending replies
|
bind index,pager r noop # to avoid accidentally sending replies
|
||||||
bind index,pager rr group-reply
|
bind index,pager rr group-reply
|
||||||
bind index,pager ro reply
|
bind index,pager ro reply
|
||||||
# open urls found in the e-mail
|
# open urls found in the e-mail
|
||||||
macro index,pager \CU "<enter-command> unset pipe_decode<enter><pipe-message>extract_url | fzf | clip<enter>" "get URLs"
|
macro index,pager \CU "|urlview<enter>" "call urlview to open links"
|
||||||
|
|
||||||
# Refresh far imap email
|
# Refresh far imap email
|
||||||
macro index O "<sync-mailbox><shell-escape>export MBSYNC_PRE=true; sync-mail<enter><sync-mailbox>" "refresh all e-mail"
|
macro index O "<sync-mailbox><shell-escape>export MBSYNC_PRE=true; sync-mail<enter><sync-mailbox>" "refresh all e-mail"
|
||||||
|
@ -84,5 +84,5 @@ macro index o "<sync-mailbox><shell-escape>export MBSYNC_PRE=true; sync-mail gma
|
||||||
|
|
||||||
|
|
||||||
# Send mail to taskwarrior
|
# Send mail to taskwarrior
|
||||||
macro index,pager ,T "<pipe-message>mutt2task -c -d -t<enter>" "add mail as task to taskwarrior with custom description and tags"
|
macro index,pager T "<pipe-message>mutt2task -c -d -t<enter>" "add mail as task to taskwarrior with custom description and tags"
|
||||||
macro index,pager ,t "<pipe-message>mutt2task -c<enter>" "add mail as task to taskwarrior"
|
macro index,pager t "<pipe-message>mutt2task -c<enter>" "add mail as task to taskwarrior"
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
export TASKOPENRC="${XDG_CONFIG_HOME:-"$HOME/.config"}/task/taskopenrc"
|
|
|
@ -1,18 +1,6 @@
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from qutebrowser.api import interceptor
|
from qutebrowser.api import interceptor
|
||||||
from qutebrowser.extensions.interceptors import RedirectException
|
|
||||||
from qutebrowser.utils import message
|
|
||||||
|
|
||||||
def fixScribePath(url):
|
|
||||||
""" Fix external medium blog to scribe translation.
|
|
||||||
Some paths from medium will go through a 'global identity'
|
|
||||||
path which messes up the actual url path we want to go
|
|
||||||
to and puts it in queries. This puts it back on the path.
|
|
||||||
"""
|
|
||||||
new_path = f"{url.path()}{url.query()}"
|
|
||||||
url.setQuery("")
|
|
||||||
url.setPath(re.sub(r"m/global-identity-2redirectUrl=", "", new_path))
|
|
||||||
|
|
||||||
redirects = {
|
redirects = {
|
||||||
"youtube": {
|
"youtube": {
|
||||||
|
@ -143,11 +131,13 @@ redirects = {
|
||||||
"rimgo.bcow.xyz",
|
"rimgo.bcow.xyz",
|
||||||
"rimgo.pussthecat.org",
|
"rimgo.pussthecat.org",
|
||||||
"rimgo.totaldarkness.net",
|
"rimgo.totaldarkness.net",
|
||||||
|
"rimgo.bus-hit.me",
|
||||||
"rimgo.esmailelbob.xyz",
|
"rimgo.esmailelbob.xyz",
|
||||||
"imgur.artemislena.eu",
|
"imgur.artemislena.eu",
|
||||||
"rimgo.vern.cc",
|
"rimgo.vern.cc",
|
||||||
"rim.odyssey346.dev",
|
"rim.odyssey346.dev",
|
||||||
"rimgo.privacytools.io",
|
"rimgo.privacytools.io",
|
||||||
|
"i.habedieeh.re",
|
||||||
"rimgo.hostux.net",
|
"rimgo.hostux.net",
|
||||||
"ri.zzls.xyz",
|
"ri.zzls.xyz",
|
||||||
"rimgo.marcopisco.com",
|
"rimgo.marcopisco.com",
|
||||||
|
@ -166,7 +156,6 @@ redirects = {
|
||||||
"scribe.privacydev.net",
|
"scribe.privacydev.net",
|
||||||
"sc.vern.cc",
|
"sc.vern.cc",
|
||||||
],
|
],
|
||||||
"postprocess": fixScribePath
|
|
||||||
},
|
},
|
||||||
"google": {
|
"google": {
|
||||||
"source": ["google.com"],
|
"source": ["google.com"],
|
||||||
|
@ -182,6 +171,7 @@ redirects = {
|
||||||
"wiki.slipfox.xyz",
|
"wiki.slipfox.xyz",
|
||||||
"wikiless.esmailelbob.xyz",
|
"wikiless.esmailelbob.xyz",
|
||||||
"wikiless.funami.tech",
|
"wikiless.funami.tech",
|
||||||
|
"wikiless.northboot.xyz",
|
||||||
"wikiless.org",
|
"wikiless.org",
|
||||||
"wikiless.tiekoetter.com",
|
"wikiless.tiekoetter.com",
|
||||||
],
|
],
|
||||||
|
@ -194,7 +184,6 @@ redirects = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def rewrite(request: interceptor.Request):
|
def rewrite(request: interceptor.Request):
|
||||||
if (
|
if (
|
||||||
request.resource_type != interceptor.ResourceType.main_frame
|
request.resource_type != interceptor.ResourceType.main_frame
|
||||||
|
@ -205,20 +194,18 @@ def rewrite(request: interceptor.Request):
|
||||||
url = request.request_url
|
url = request.request_url
|
||||||
|
|
||||||
for service in redirects.values():
|
for service in redirects.values():
|
||||||
matched = False
|
matched=False
|
||||||
for source in service["source"]:
|
for source in service["source"]:
|
||||||
if re.search(source, url.host()):
|
if re.search(source, url.host()):
|
||||||
matched = True
|
matched=True
|
||||||
|
|
||||||
if matched:
|
if matched:
|
||||||
target = service["target"][random.randint(0, len(service["target"]) - 1)]
|
target = service["target"][random.randint(0, len(service["target"])-1)]
|
||||||
if target is not None and url.setHost(target) is not False:
|
if target is not None and url.setHost(target) is not False:
|
||||||
if "postprocess" in service:
|
|
||||||
service["postprocess"](url)
|
|
||||||
try:
|
try:
|
||||||
request.redirect(url)
|
request.redirect(url)
|
||||||
except RedirectException as e:
|
except:
|
||||||
message.error(str(e))
|
pass
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ setup() {
|
||||||
DL_FOLDER="${DL_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/inbox}"
|
DL_FOLDER="${DL_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/inbox}"
|
||||||
ARCHIVE_FOLDER="${ARCHIVE_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/archive}"
|
ARCHIVE_FOLDER="${ARCHIVE_FOLDER:-${XDG_VIDEOS_DIR:-$HOME/videos}/archive}"
|
||||||
YT_DL_CMD="${YT_DL_CMD:-yt-dlp}"
|
YT_DL_CMD="${YT_DL_CMD:-yt-dlp}"
|
||||||
yt_default_opts=(-f "best[height\<=1080]" --retries 15 --embed-chapters --embed-subs --sub-lang "en,de,es,fr")
|
yt_default_opts=(-f "best[height\<=1080]" --retries 15 --embed-subs --sub-lang "en,de,es,fr")
|
||||||
declare -a YT_DL_OPTS=${YT_DL_OPTS:-( "${yt_default_opts[@]}" )}
|
declare -a YT_DL_OPTS=${YT_DL_OPTS:-( "${yt_default_opts[@]}" )}
|
||||||
YT_DL_TITLE="${YT_DL_TITLE:-%(channel)s_%(title)s_%(id)s}" # this title needs to be without extension
|
YT_DL_TITLE="${YT_DL_TITLE:-%(channel)s_%(title)s_%(id)s}" # this title needs to be without extension
|
||||||
|
|
||||||
|
|
|
@ -74,9 +74,9 @@ bind-key h quit searchresultslist
|
||||||
bind-key g home
|
bind-key g home
|
||||||
bind-key G end
|
bind-key G end
|
||||||
bind-key ^F pagedown
|
bind-key ^F pagedown
|
||||||
|
bind-key ^B pageup
|
||||||
|
bind-key ^D pagedown
|
||||||
bind-key ^U pageup
|
bind-key ^U pageup
|
||||||
bind-key ^B halfpageup
|
|
||||||
bind-key ^D halfpagedown
|
|
||||||
bind-key n next-unread
|
bind-key n next-unread
|
||||||
bind-key N prev-unread
|
bind-key N prev-unread
|
||||||
bind-key ^n next-unread-feed articlelist
|
bind-key ^n next-unread-feed articlelist
|
||||||
|
|
|
@ -5,7 +5,9 @@ local act = wezterm.action
|
||||||
|
|
||||||
local function setup()
|
local function setup()
|
||||||
local function isViProcess(pane)
|
local function isViProcess(pane)
|
||||||
return pane:get_foreground_process_name():find('n?vim') ~= nil
|
local proc = pane:get_foreground_process_name()
|
||||||
|
if (proc:find('vim') or proc:find('nvim')) then return true end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function conditionalActivatePane(window, pane, pane_direction,
|
local function conditionalActivatePane(window, pane, pane_direction,
|
||||||
|
|
|
@ -36,35 +36,6 @@ local settings = {
|
||||||
-- default_prog = {"nu"},
|
-- default_prog = {"nu"},
|
||||||
scrollback_lines = 10000,
|
scrollback_lines = 10000,
|
||||||
font = wezterm.font('Iosevka Nerd Font'),
|
font = wezterm.font('Iosevka Nerd Font'),
|
||||||
-- add cursive italic font from Victor font for all weights
|
|
||||||
font_rules = {
|
|
||||||
{
|
|
||||||
intensity = 'Bold',
|
|
||||||
italic = true,
|
|
||||||
font = wezterm.font {
|
|
||||||
family = 'VictorMono Nerd Font',
|
|
||||||
weight = 'Bold',
|
|
||||||
style = 'Italic',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
italic = true,
|
|
||||||
intensity = 'Half',
|
|
||||||
font = wezterm.font {
|
|
||||||
family = 'VictorMono Nerd Font',
|
|
||||||
weight = 'DemiBold',
|
|
||||||
style = 'Italic',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
italic = true,
|
|
||||||
intensity = 'Normal',
|
|
||||||
font = wezterm.font {
|
|
||||||
family = 'VictorMono Nerd Font',
|
|
||||||
style = 'Italic',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
line_height = 1.0,
|
line_height = 1.0,
|
||||||
leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1500 },
|
leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1500 },
|
||||||
keys = maps.keys,
|
keys = maps.keys,
|
||||||
|
@ -74,8 +45,7 @@ local settings = {
|
||||||
event = { Up = { streak = 1, button = 'Left' } },
|
event = { Up = { streak = 1, button = 'Left' } },
|
||||||
mods = 'NONE',
|
mods = 'NONE',
|
||||||
action = wezterm.action
|
action = wezterm.action
|
||||||
.CompleteSelectionOrOpenLinkAtMouseCursor
|
.CompleteSelectionOrOpenLinkAtMouseCursor 'ClipboardAndPrimarySelection'
|
||||||
'ClipboardAndPrimarySelection'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue