3792 lines
158 KiB
Lua
3792 lines
158 KiB
Lua
-- ModernZ (https://github.com/Samillion/ModernZ)
|
|
--
|
|
-- This script is a derivative of the original mpv-osc-modern by maoiscat
|
|
-- and subsequent forks:
|
|
-- * cyl0/ModernX
|
|
-- * dexeonify/ModernX
|
|
--
|
|
-- It is based on the official osc.lua from mpv, licensed under the
|
|
-- GNU Lesser General Public License v2.1 (LGPLv2.1).
|
|
-- Full license: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
|
|
|
|
local assdraw = require "mp.assdraw"
|
|
local msg = require "mp.msg"
|
|
local opt = require "mp.options"
|
|
local utils = require "mp.utils"
|
|
|
|
-- Parameters
|
|
-- default user option values
|
|
-- do not touch, change them in modernz.conf
|
|
local user_opts = {
|
|
-- Language and display
|
|
language = "en", -- set language (for available options, see: https://github.com/Samillion/ModernZ/blob/main/docs/TRANSLATIONS.md)
|
|
icon_theme = "fluent", -- set icon theme. accepts "fluent" or "material"
|
|
font = "mpv-osd-symbols", -- font for the OSC (default: mpv-osd-symbols or the one set in mpv.conf)
|
|
|
|
idlescreen = true, -- show mpv logo when idle
|
|
window_top_bar = "auto", -- show OSC window top bar: "auto", "yes", or "no" (borderless/fullscreen)
|
|
showwindowed = true, -- show OSC when windowed
|
|
showfullscreen = true, -- show OSC when fullscreen
|
|
showonpause = true, -- show OSC when paused
|
|
keeponpause = true, -- disable OSC hide timeout when paused
|
|
greenandgrumpy = false, -- disable Santa hat in December
|
|
|
|
-- OSC behaviour and scaling
|
|
hidetimeout = 1500, -- time (in ms) before OSC hides if no mouse movement
|
|
seek_resets_hidetimeout = true, -- if seeking should reset the hidetimeout
|
|
fadeduration = 200, -- fade-out duration (in ms), set to 0 for no fade
|
|
fadein = false, -- whether to enable fade-in effect
|
|
minmousemove = 0, -- minimum mouse movement (in pixels) required to show OSC
|
|
bottomhover = true, -- show OSC only when hovering at the bottom
|
|
bottomhover_zone = 130, -- height of hover zone for bottomhover (in pixels)
|
|
osc_on_seek = false, -- show OSC when seeking
|
|
osc_on_start = false, -- show OSC on start of every file
|
|
mouse_seek_pause = true, -- pause video while seeking with mouse move (on button hold)
|
|
force_seek_tooltip = false, -- force show seekbar tooltip on mouse drag, even if not hovering seekbar
|
|
|
|
vidscale = "auto", -- scale osc with the video
|
|
scalewindowed = 1.0, -- osc scale factor when windowed
|
|
scalefullscreen = 1.0, -- osc scale factor when fullscreen
|
|
|
|
-- Elements display
|
|
show_title = true, -- show title in the OSC (above seekbar)
|
|
title = "${media-title}", -- title above seekbar format: "${media-title}" or "${filename}"
|
|
title_font_size = 24, -- title font size (above seekbar)
|
|
chapter_title_font_size = 14, -- chapter title font size
|
|
|
|
cache_info = false, -- show cached time information
|
|
cache_info_speed = false, -- show cache speed per second
|
|
cache_info_font_size = 12, -- font size of the cache information
|
|
|
|
show_chapter_title = true, -- show chapter title (above seekbar)
|
|
chapter_fmt = "%s", -- format for chapter display on seekbar hover (set to "no" to disable)
|
|
|
|
timetotal = true, -- show total time instead of remaining time
|
|
timems = false, -- show timecodes with milliseconds
|
|
unicodeminus = false, -- use the Unicode minus sign in remaining time
|
|
time_format = "dynamic", -- "dynamic" or "fixed". dynamic shows MM:SS when possible, fixed always shows HH:MM:SS
|
|
time_font_size = 16, -- font size of the time display
|
|
|
|
tooltip_font_size = 14, -- tooltips font size
|
|
|
|
-- Title bar settings
|
|
window_title = false, -- show window title in borderless/fullscreen mode
|
|
window_controls = true, -- show window controls (close, minimize, maximize) in borderless/fullscreen
|
|
windowcontrols_title = "${media-title}", -- same as title but for windowcontrols
|
|
|
|
-- Subtitle display settings
|
|
raise_subtitles = true, -- raise subtitles above the OSC when shown
|
|
raise_subtitle_amount = 125, -- amount by which subtitles are raised when the OSC is shown (in pixels)
|
|
|
|
-- Buttons display and functionality
|
|
jump_buttons = true, -- show the jump backward and forward buttons
|
|
jump_amount = 10, -- change the jump amount in seconds
|
|
jump_more_amount = 60, -- change the jump amount in seconds when right-clicking jump buttons and shift-clicking chapter skip buttons
|
|
jump_icon_number = true, -- show different icon when jump_amount is set to 5, 10, or 30
|
|
jump_mode = "relative", -- seek mode for jump buttons
|
|
jump_softrepeat = true, -- enable continuous jumping when holding down seek buttons
|
|
chapter_skip_buttons = false, -- show the chapter skip backward and forward buttons
|
|
chapter_softrepeat = true, -- enable continuous skipping when holding down chapter skip buttons
|
|
track_nextprev_buttons = true, -- show next/previous playlist track buttons
|
|
|
|
volume_control = true, -- show mute button and volume slider
|
|
volume_control_type = "linear", -- volume scale type: "linear" or "logarithmic"
|
|
playlist_button = true, -- show playlist button: Left-click for simple playlist, Right-click for interactive playlist
|
|
hide_empty_playlist_button = false, -- hide playlist button when no playlist exists
|
|
gray_empty_playlist_button = false, -- gray out the playlist button when no playlist exists
|
|
|
|
fullscreen_button = true, -- show fullscreen toggle button
|
|
info_button = true, -- show info button
|
|
ontop_button = true, -- show window on top button
|
|
screenshot_button = false, -- show screenshot button
|
|
screenshot_flag = "subtitles", -- flag for screenshot button: "subtitles", "video", "window", "each-frame"
|
|
-- https://mpv.io/manual/master/#command-interface-screenshot-%3Cflags%3E
|
|
|
|
download_button = true, -- show download button on web videos (requires yt-dlp and ffmpeg)
|
|
download_path = "~~desktop/mpv", -- default download directory for videos (https://mpv.io/manual/master/#paths)
|
|
|
|
loop_button = false, -- show loop button
|
|
speed_button = false, -- show speed control button
|
|
speed_button_click = 1, -- speed change amount per click
|
|
speed_button_scroll = 0.25, -- speed change amount on scroll
|
|
|
|
loop_in_pause = true, -- enable looping by right-clicking pause
|
|
|
|
buttons_always_active = "none", -- force buttons to always be active. can add: playlist_prev, playlist_next
|
|
|
|
playpause_size = 28, -- icon size for the play/pause button
|
|
midbuttons_size = 24, -- icon size for the middle buttons
|
|
sidebuttons_size = 24, -- icon size for the side buttons
|
|
|
|
zoom_control = true, -- show zoom controls in image viewer mode
|
|
zoom_in_max = 4, -- maximum zoom in value
|
|
zoom_out_min = -1, -- minimum zoom out value
|
|
|
|
-- Colors and style
|
|
osc_color = "#000000", -- accent color of the OSC and title bar
|
|
window_title_color = "#FFFFFF", -- color of the title in borderless/fullscreen mode
|
|
window_controls_color = "#FFFFFF", -- color of the window controls (close, minimize, maximize) in borderless/fullscreen mode
|
|
windowcontrols_close_hover = "#F45C5B", -- color of close window control on hover
|
|
windowcontrols_max_hover = "#F8BC3A", -- color of maximize window controls on hover
|
|
windowcontrols_min_hover = "#43CB44", -- color of minimize window controls on hover
|
|
title_color = "#FFFFFF", -- color of the title (above seekbar)
|
|
cache_info_color = "#FFFFFF", -- color of the cache information
|
|
seekbarfg_color = "#FB8C00", -- color of the seekbar progress and handle
|
|
seekbarbg_color = "#94754F", -- color of the remaining seekbar
|
|
seekbar_cache_color = "#918F8E", -- color of the cache ranges on the seekbar
|
|
volumebar_match_seek_color = false, -- match volume bar color with seekbar color (ignores side_buttons_color)
|
|
time_color = "#FFFFFF", -- color of the timestamps (below seekbar)
|
|
chapter_title_color = "#FFFFFF", -- color of the chapter title (above seekbar)
|
|
side_buttons_color = "#FFFFFF", -- color of the side buttons (audio, subtitles, playlist, etc.)
|
|
middle_buttons_color = "#FFFFFF", -- color of the middle buttons (skip, jump, chapter, etc.)
|
|
playpause_color = "#FFFFFF", -- color of the play/pause button
|
|
held_element_color = "#999999", -- color of the element when held down (pressed)
|
|
hover_effect_color = "#FB8C00", -- color of a hovered button when hover_effect includes "color"
|
|
thumbnail_border_color = "#111111", -- color of the border for thumbnails (with thumbfast)
|
|
thumbnail_border_outline = "#404040", -- color of the border outline for thumbnails
|
|
|
|
fade_alpha = 130, -- alpha of the OSC background (0 to disable)
|
|
fade_blur_strength = 100, -- blur strength for the OSC alpha fade. caution: high values can take a lot of CPU time to render
|
|
fade_transparency_strength = 0, -- use with "fade_blur_strength=0" to create a transparency box
|
|
window_fade_alpha = 100, -- alpha of the window title bar (0 to disable)
|
|
window_fade_blur_strength = 100, -- blur strength for the window title bar. caution: high values can take a lot of CPU time to render
|
|
window_fade_transparency_strength = 0, -- use with "window_fade_blur_strength=0" to create a transparency box
|
|
thumbnail_border = 3, -- width of the thumbnail border (for thumbfast)
|
|
thumbnail_border_radius = 3, -- rounded corner radius for thumbnail border (0 to disable)
|
|
|
|
-- Button hover effects
|
|
hover_effect = "size,glow,color", -- active button hover effects: "glow", "size", "color"; can use multiple separated by commas
|
|
hover_button_size = 115, -- relative size of a hovered button if "size" effect is active
|
|
button_glow_amount = 5, -- glow intensity when "glow" hover effect is active
|
|
hover_effect_for_sliders = true, -- apply size hover effect to slider handles
|
|
|
|
-- Tooltips and hints
|
|
tooltips_for_disabled_elements = true, -- enable tooltips for disabled buttons and elements
|
|
tooltip_hints = true, -- enable text hints for info, loop, ontop, and screenshot buttons
|
|
|
|
-- Progress bar settings
|
|
seek_handle_size = 0.8, -- size ratio of the seek handle (range: 0 ~ 1)
|
|
seekrange = true, -- show seek range overlay
|
|
seekrangealpha = 150, -- transparency of the seek range
|
|
livemarkers = true, -- update chapter markers on the seekbar when duration changes
|
|
seekbarkeyframes = false, -- use keyframes when dragging the seekbar
|
|
|
|
nibbles_top = true, -- top chapter nibbles above seekbar
|
|
nibbles_bottom = true, -- bottom chapter nibbles below seekbar
|
|
nibbles_style = "triangle", -- chapter nibble style. "triangle", "bar", or "single-bar"
|
|
|
|
automatickeyframemode = true, -- automatically set keyframes for the seekbar based on video length
|
|
automatickeyframelimit = 600, -- videos longer than this (in seconds) will have keyframes on the seekbar
|
|
|
|
persistentprogress = false, -- always show a small progress line at the bottom of the screen
|
|
persistentprogressheight = 17, -- height of the persistent progress bar
|
|
persistentbuffer = false, -- show buffer status on web videos in the persistent progress line
|
|
|
|
-- Miscellaneous settings
|
|
visibility = "auto", -- only used at init to set visibility_mode(...)
|
|
visibility_modes = "never_auto_always",-- visibility modes to cycle through
|
|
tick_delay = 0.03, -- minimum interval between OSC redraws (in seconds)
|
|
tick_delay_follow_display_fps = false, -- use display FPS as the minimum redraw interval
|
|
|
|
-- Elements Position
|
|
-- Useful when adjusting font size or type
|
|
title_height = 96, -- title height position above seekbar
|
|
title_with_chapter_height = 108, -- title height position if a chapter title is below it
|
|
chapter_title_height = 91, -- chapter title height position above seekbar
|
|
time_codes_height = 35, -- time codes height position
|
|
time_codes_centered_height = 57, -- time codes height position with portrait window
|
|
tooltip_height_offset = 2, -- tooltip height position offset
|
|
tooltip_left_offset = 5, -- if tooltip contains many characters, it is moved to the left by offset
|
|
portrait_window_trigger = 1000, -- portrait window width trigger to move some elements
|
|
hide_volume_bar_trigger = 1150, -- hide volume bar trigger window width
|
|
notitle_osc_h_offset = 25, -- osc height offset if title above seekbar is disabled
|
|
nochapter_osc_h_offset = 10, -- osc height offset if chapter title is disabled or doesn't exist
|
|
seek_hover_tooltip_h_offset = 0, -- seek hover timecodes tooltip height position offset
|
|
osc_height = 132, -- osc height without offsets
|
|
|
|
-- Mouse commands
|
|
-- customize the button function based on mouse action
|
|
|
|
-- title above seekbar mouse actions
|
|
title_mbtn_left_command = "script-binding stats/display-page-5",
|
|
title_mbtn_mid_command = "show-text ${path}",
|
|
title_mbtn_right_command = "script-binding select/select-watch-history; script-message-to modernz osc-hide",
|
|
|
|
-- playlist button mouse actions
|
|
playlist_mbtn_left_command = "script-binding select/menu; script-message-to modernz osc-hide",
|
|
playlist_mbtn_right_command = "script-binding select/select-playlist; script-message-to modernz osc-hide",
|
|
|
|
-- volume mouse actions
|
|
vol_ctrl_mbtn_left_command = "no-osd cycle mute",
|
|
vol_ctrl_mbtn_right_command = "script-binding select/select-audio-device; script-message-to modernz osc-hide",
|
|
vol_ctrl_wheel_down_command = "no-osd add volume -5",
|
|
vol_ctrl_wheel_up_command = "no-osd add volume 5",
|
|
|
|
-- audio button mouse actions
|
|
audio_track_mbtn_left_command = "script-binding select/select-aid; script-message-to modernz osc-hide",
|
|
audio_track_mbtn_mid_command = "cycle audio down",
|
|
audio_track_mbtn_right_command = "cycle audio",
|
|
audio_track_wheel_down_command = "cycle audio",
|
|
audio_track_wheel_up_command = "cycle audio down",
|
|
|
|
-- subtitle button mouse actions
|
|
sub_track_mbtn_left_command = "script-binding select/select-sid; script-message-to modernz osc-hide",
|
|
sub_track_mbtn_mid_command = "cycle sub down",
|
|
sub_track_mbtn_right_command = "cycle sub",
|
|
sub_track_wheel_down_command = "cycle sub",
|
|
sub_track_wheel_up_command = "cycle sub down",
|
|
|
|
-- chapter skip buttons mouse actions
|
|
chapter_prev_mbtn_left_command = "add chapter -1",
|
|
chapter_prev_mbtn_mid_command = "show-text ${chapter-list} 3000",
|
|
chapter_prev_mbtn_right_command = "script-binding select/select-chapter; script-message-to modernz osc-hide",
|
|
|
|
chapter_next_mbtn_left_command = "add chapter 1",
|
|
chapter_next_mbtn_mid_command = "show-text ${chapter-list} 3000",
|
|
chapter_next_mbtn_right_command = "script-binding select/select-chapter; script-message-to modernz osc-hide",
|
|
|
|
-- chapter title (below seekbar) mouse actions
|
|
chapter_title_mbtn_left_command = "script-binding select/select-chapter; script-message-to modernz osc-hide",
|
|
chapter_title_mbtn_right_command = "show-text ${chapter-list} 3000",
|
|
|
|
-- playlist skip buttons mouse actions
|
|
playlist_prev_mbtn_left_command = "playlist-prev",
|
|
playlist_prev_mbtn_mid_command = "show-text ${playlist} 3000",
|
|
playlist_prev_mbtn_right_command = "script-binding select/select-playlist; script-message-to modernz osc-hide",
|
|
|
|
playlist_next_mbtn_left_command = "playlist-next",
|
|
playlist_next_mbtn_mid_command = "show-text ${playlist} 3000",
|
|
playlist_next_mbtn_right_command = "script-binding select/select-playlist; script-message-to modernz osc-hide",
|
|
|
|
-- fullscreen button mouse actions
|
|
fullscreen_mbtn_left_command = "cycle fullscreen",
|
|
fullscreen_mbtn_right_command = "cycle window-maximized",
|
|
|
|
-- info button mouse actions
|
|
info_mbtn_left_command = "script-binding stats/display-page-1-toggle",
|
|
}
|
|
|
|
mp.observe_property("osc", "bool", function(name, value) if value == true then mp.set_property("osc", "no") end end)
|
|
|
|
local osc_param = { -- calculated by osc_init()
|
|
playresy = 0, -- canvas size Y
|
|
playresx = 0, -- canvas size X
|
|
display_aspect = 1,
|
|
unscaled_y = 0,
|
|
areas = {},
|
|
video_margins = {
|
|
l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom
|
|
},
|
|
}
|
|
|
|
local icon_theme = {
|
|
["fluent"] = {
|
|
iconfont = "fluent-system-icons",
|
|
window = {
|
|
maximize = "\238\159\171",
|
|
unmaximize = "\238\174\150",
|
|
minimize = "\238\175\144",
|
|
close = "\239\141\169",
|
|
},
|
|
audio = "\238\175\139",
|
|
subtitle = "\238\175\141",
|
|
playlist = "\238\161\159",
|
|
menu = "\238\160\170",
|
|
volume_mute = "\238\173\138",
|
|
volume_quiet = "\238\172\184",
|
|
volume_low = "\238\172\189",
|
|
volume_high = "\238\173\130",
|
|
|
|
play = "\238\166\143",
|
|
pause = "\238\163\140",
|
|
replay = "\238\189\191",
|
|
previous = "\239\152\167",
|
|
next = "\239\149\168",
|
|
rewind = "\238\168\158",
|
|
forward = "\238\152\135",
|
|
jump = {
|
|
[5] = {"\238\171\186", "\238\171\187"},
|
|
[10] = {"\238\171\188", "\238\172\129"},
|
|
[30] = {"\238\172\133", "\238\172\134"},
|
|
default = {"\238\172\138", "\238\172\138"}, -- second icon is mirrored in layout()
|
|
},
|
|
|
|
fullscreen = "\239\133\160",
|
|
fullscreen_exit = "\239\133\166",
|
|
info = "\239\146\164",
|
|
ontop_on = "\238\165\190",
|
|
ontop_off = "\238\166\129",
|
|
screenshot = "\238\169\150",
|
|
loop_off = "\239\133\178",
|
|
loop_on = "\239\133\181",
|
|
speed = "\239\160\177",
|
|
download = "\239\133\144",
|
|
downloading = "\239\140\174",
|
|
|
|
zoom_in = "\238\186\142",
|
|
zoom_out = "\238\186\143",
|
|
},
|
|
["material"] = {
|
|
iconfont = "Material Design Icons",
|
|
window = {
|
|
maximize = '\243\176\150\175',
|
|
unmaximize = '\243\176\150\178',
|
|
minimize = '\243\176\150\176',
|
|
close = '\243\176\150\173',
|
|
},
|
|
audio = '\243\176\151\133',
|
|
subtitle = '\243\176\168\150',
|
|
playlist = '\243\176\141\156', -- this icon is better suited as a generic menu button
|
|
menu = '\243\176\149\178', -- this icon would be better suited for playlists
|
|
volume_mute = '\243\176\184\136',
|
|
volume_quiet = '\243\176\149\191',
|
|
volume_low = '\243\176\150\128',
|
|
volume_high = '\243\176\149\190',
|
|
|
|
play = '\243\176\144\138',
|
|
pause = '\243\176\143\164',
|
|
replay = '\243\176\145\153',
|
|
previous = '\243\176\146\171',
|
|
next = '\243\176\146\172',
|
|
rewind = '\243\176\145\159',
|
|
forward = '\243\176\136\145',
|
|
jump = {
|
|
[5] = {'\243\176\135\185', '\243\176\135\184'},
|
|
[10] = {'\243\176\180\170', '\243\176\181\177'},
|
|
[30] = {'\243\176\182\150', '\243\176\180\134'},
|
|
default = {'\243\176\147\151', '\243\176\147\151'}, -- first would be '\243\176\147\149' but icon is mirrored in layout()
|
|
},
|
|
|
|
fullscreen = '\243\176\138\147',
|
|
fullscreen_exit = '\243\176\138\148',
|
|
info = '\243\176\139\189',
|
|
ontop_on = '\243\176\144\131',
|
|
ontop_off = '\243\176\164\176',
|
|
screenshot = '\243\176\132\128',
|
|
loop_off = '\243\176\145\151',
|
|
loop_on = '\243\176\145\150',
|
|
speed = '\243\176\163\191',
|
|
download = '\243\176\129\136',
|
|
downloading = '\243\176\166\151',
|
|
|
|
zoom_in = '\243\176\155\173',
|
|
zoom_out = '\243\176\155\172',
|
|
},
|
|
}
|
|
|
|
--- localization
|
|
local language = {
|
|
["en"] = {
|
|
idle = "Drop files or URLs here to play",
|
|
na = "Not available",
|
|
video = "Video",
|
|
audio = "Audio",
|
|
subtitle = "Subtitle",
|
|
no_subs = "No subtitles available",
|
|
no_audio = "No audio tracks available",
|
|
playlist = "Playlist",
|
|
no_playlist = "Playlist is empty",
|
|
chapter = "Chapter",
|
|
ontop = "Pin Window",
|
|
ontop_disable = "Unpin Window",
|
|
loop_enable = "Loop",
|
|
loop_disable = "Disable Loop",
|
|
speed_control = "Speed Control",
|
|
screenshot = "Screenshot",
|
|
stats_info = "Information",
|
|
cache = "Cache",
|
|
buffering = "Buffering",
|
|
zoom_in = "Zoom In",
|
|
zoom_out = "Zoom Out",
|
|
download = "Download",
|
|
download_in_progress = "Download in progress",
|
|
downloading = "Downloading",
|
|
downloaded = "Already downloaded",
|
|
menu = "Menu",
|
|
},
|
|
}
|
|
|
|
-- locale JSON file handler
|
|
function get_locale_from_json(path)
|
|
local expand_path = mp.command_native({'expand-path', path})
|
|
|
|
local file_info = utils.file_info(expand_path)
|
|
if not file_info or not file_info.is_file then
|
|
return nil
|
|
end
|
|
|
|
local json_file = io.open(expand_path, 'r')
|
|
if not json_file then
|
|
return nil
|
|
end
|
|
|
|
local json = json_file:read('*all')
|
|
json_file:close()
|
|
|
|
local json_table, parse_error = utils.parse_json(json)
|
|
if not json_table then
|
|
mp.msg.error("JSON parse error:" .. parse_error)
|
|
end
|
|
return json_table
|
|
end
|
|
|
|
-- load external locales if available
|
|
local locale_path = "~~/script-opts/modernz-locale.json"
|
|
local external = get_locale_from_json(locale_path)
|
|
|
|
if external then
|
|
for lang, strings in pairs(external) do
|
|
if type(strings) == "table" then
|
|
language[lang] = strings
|
|
|
|
-- fill in missing locales with English defaults
|
|
for key, value in pairs(language["en"]) do
|
|
if strings[key] == nil then
|
|
strings[key] = value or "" -- fallback to empty string if key is missing
|
|
end
|
|
|
|
-- debug log to verify all keys are populated
|
|
if strings[key] == nil then
|
|
mp.msg.warn("Locale key '" .. key .. "' is nil in language: " .. lang)
|
|
end
|
|
end
|
|
else
|
|
mp.msg.warn("Locale data for language " .. lang .. " is not in the correct format.")
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local icons
|
|
local iconfont
|
|
local function set_icon_theme()
|
|
icons = icon_theme[user_opts.icon_theme] or icon_theme["fluent"]
|
|
iconfont = icons.iconfont
|
|
end
|
|
|
|
local locale
|
|
local function set_osc_locale()
|
|
locale = language[user_opts.language] or language["en"]
|
|
local idle_ass_tags = "{\\fs24\\1c&H0&\\1c&HFFFFFF&}"
|
|
locale.idle = idle_ass_tags .. locale.idle
|
|
end
|
|
|
|
local function contains(list, item)
|
|
local t = type(list) == "table" and list or {}
|
|
if type(list) ~= "table" then
|
|
for str in string.gmatch(list, '([^,]+)') do
|
|
t[#t + 1] = str:match("^%s*(.-)%s*$") -- trim spaces
|
|
end
|
|
end
|
|
for _, v in ipairs(t) do
|
|
if v == item then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local thumbfast = {
|
|
width = 0,
|
|
height = 0,
|
|
disabled = true,
|
|
available = false
|
|
}
|
|
|
|
local tick_delay = 1 / 60
|
|
local audio_track_count = 0
|
|
local sub_track_count = 0
|
|
local window_control_box_width = 150
|
|
local is_december = os.date("*t").month == 12
|
|
local UNICODE_MINUS = string.char(0xe2, 0x88, 0x92) -- UTF-8 for U+2212 MINUS SIGN
|
|
|
|
local function osc_color_convert(color)
|
|
return color:sub(6,7) .. color:sub(4,5) .. color:sub(2,3)
|
|
end
|
|
|
|
local osc_styles
|
|
|
|
local function set_osc_styles()
|
|
local playpause_size = user_opts.playpause_size or 28
|
|
local midbuttons_size = user_opts.midbuttons_size or 24
|
|
local sidebuttons_size = user_opts.sidebuttons_size or 24
|
|
osc_styles = {
|
|
osc_fade_bg = "{\\blur" .. user_opts.fade_blur_strength .. "\\bord" .. user_opts.fade_alpha .. "\\1c&H0&\\3c&H" .. osc_color_convert(user_opts.osc_color) .. "&}",
|
|
window_fade_bg = "{\\blur" .. user_opts.window_fade_blur_strength .. "\\bord" .. user_opts.window_fade_alpha .. "\\1c&H0&\\3c&H" .. osc_color_convert(user_opts.osc_color) .. "&}",
|
|
window_control = "{\\blur1\\bord0.5\\1c&H" .. osc_color_convert(user_opts.window_controls_color) .. "&\\3c&H0&\\fs25\\fn" .. iconfont .. "}",
|
|
window_title = "{\\blur1\\bord0.5\\1c&H" .. osc_color_convert(user_opts.window_title_color) .. "&\\3c&H0&\\fs26\\q2\\fn" .. user_opts.font .. "}",
|
|
title = "{\\blur1\\bord0.5\\1c&H" .. osc_color_convert(user_opts.title_color) .. "&\\3c&H0&\\fs".. user_opts.title_font_size .."\\q2\\fn" .. user_opts.font .. "}",
|
|
chapter_title = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.chapter_title_color) .. "&\\3c&H0&\\fs" .. user_opts.chapter_title_font_size .. "\\fn" .. user_opts.font .. "}",
|
|
seekbar_bg = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.seekbarbg_color) .. "&}",
|
|
seekbar_fg = "{\\blur1\\bord1\\1c&H" .. osc_color_convert(user_opts.seekbarfg_color) .. "&}",
|
|
thumbnail = "{\\blur0\\bord1\\1c&H" .. osc_color_convert(user_opts.thumbnail_border_color) .. "&\\3c&H" .. osc_color_convert(user_opts.thumbnail_border_outline) .. "&}",
|
|
time = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.time_color) .. "&\\3c&H0&\\fs" .. user_opts.time_font_size .. "\\fn" .. user_opts.font .. "}",
|
|
cache = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.cache_info_color) .. "&\\3c&H0&\\fs" .. user_opts.cache_info_font_size .. "\\fn" .. user_opts.font .. "}",
|
|
tooltip = "{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0&\\fs" .. user_opts.tooltip_font_size .. "\\fn" .. user_opts.font .. "}",
|
|
volumebar_bg = "{\\blur0\\bord0\\1c&H999999&}",
|
|
volumebar_fg = "{\\blur1\\bord1\\1c&H" .. osc_color_convert(user_opts.side_buttons_color) .. "&}",
|
|
control_1 = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.playpause_color) .. "&\\3c&HFFFFFF&\\fs" .. playpause_size .. "\\fn" .. iconfont .. "}",
|
|
control_2 = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.middle_buttons_color) .. "&\\3c&HFFFFFF&\\fs" .. midbuttons_size .. "\\fn" .. iconfont .. "}",
|
|
control_2_flip = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.middle_buttons_color) .. "&\\3c&HFFFFFF&\\fs" .. midbuttons_size .. "\\fn" .. iconfont .. "\\fry180}",
|
|
control_3 = "{\\blur0\\bord0\\1c&H" .. osc_color_convert(user_opts.side_buttons_color) .. "&\\3c&HFFFFFF&\\fs" .. sidebuttons_size .. "\\fn" .. iconfont .. "}",
|
|
element_down = "{\\1c&H" .. osc_color_convert(user_opts.held_element_color) .. "&}",
|
|
element_hover = "{" .. (contains(user_opts.hover_effect, "color") and "\\1c&H" .. osc_color_convert(user_opts.hover_effect_color) .. "&" or "") .."\\2c&HFFFFFF&" .. (contains(user_opts.hover_effect, "size") and string.format("\\fscx%s\\fscy%s", user_opts.hover_button_size, user_opts.hover_button_size) or "") .. "}",
|
|
}
|
|
end
|
|
|
|
-- internal states, do not touch
|
|
local state = {
|
|
showtime = nil, -- time of last invocation (last mouse move)
|
|
touchtime = nil, -- time of last invocation (last touch event)
|
|
osc_visible = false,
|
|
anistart = nil, -- time when the animation started
|
|
anitype = nil, -- current type of animation
|
|
animation = nil, -- current animation alpha
|
|
mouse_down_counter = 0, -- used for softrepeat
|
|
active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
|
|
active_event_source = nil, -- the "button" that issued the current event
|
|
tc_right_rem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
|
|
tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
|
|
screen_sizeX = nil, screen_sizeY = nil, -- last screen-resolution, to detect resolution changes to issue reINITs
|
|
initREQ = false, -- is a re-init request pending?
|
|
marginsREQ = false, -- is a margins update pending?
|
|
last_mouseX = nil, last_mouseY = nil, -- last mouse position, to detect significant mouse movement
|
|
mouse_in_window = false,
|
|
fullscreen = false,
|
|
tick_timer = nil,
|
|
tick_last_time = 0, -- when the last tick() was run
|
|
hide_timer = nil,
|
|
cache_state = nil,
|
|
idle = false,
|
|
enabled = true,
|
|
input_enabled = true,
|
|
showhide_enabled = false,
|
|
windowcontrols_buttons = false,
|
|
windowcontrols_title = false,
|
|
dmx_cache = 0,
|
|
border = true,
|
|
maximized = false,
|
|
osd = mp.create_osd_overlay("ass-events"),
|
|
buffering = false,
|
|
new_file_flag = false, -- flag to detect new file starts
|
|
temp_visibility_mode = nil, -- store temporary visibility mode state
|
|
chapter_list = {}, -- sorted by time
|
|
visibility_modes = {}, -- visibility_modes to cycle through
|
|
mute = false,
|
|
looping = false,
|
|
sliderpos = 0,
|
|
touchingprogressbar = false, -- if the mouse is touching the progress bar
|
|
initialborder = mp.get_property("border"),
|
|
playtime_hour_force_init = false, -- used to force request_init() once
|
|
playtime_nohour_force_init = false, -- used to force request_init() once
|
|
playing_and_seeking = false,
|
|
persistent_progress_toggle = user_opts.persistentprogress,
|
|
user_subpos = mp.get_property_number("sub-pos") or 100,
|
|
osc_adjusted_subpos = nil,
|
|
downloaded_once = false,
|
|
downloading = false,
|
|
file_size_bytes = 0,
|
|
file_size_normalized = "Approximating size...",
|
|
is_URL = false,
|
|
is_image = false,
|
|
url_path = "", -- used for yt-dlp downloading
|
|
}
|
|
|
|
local logo_lines = {
|
|
-- White border
|
|
"{\\c&HE5E5E5&\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 {\\p0}",
|
|
-- Purple fill
|
|
"{\\c&H682167&\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42{\\p0}",
|
|
-- Darker fill
|
|
"{\\c&H430142&\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}",
|
|
-- White fill
|
|
"{\\c&HDDDBDD&\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}",
|
|
-- Triangle
|
|
"{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}",
|
|
}
|
|
|
|
local santa_hat_lines = {
|
|
-- Pompoms
|
|
"{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}",
|
|
"{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}",
|
|
-- Main cap
|
|
"{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}",
|
|
-- Cap shadow
|
|
"{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}",
|
|
-- Brim and tip pompom
|
|
"{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}",
|
|
}
|
|
|
|
--
|
|
-- Helper functions
|
|
--
|
|
|
|
local function kill_animation()
|
|
state.anistart = nil
|
|
state.animation = nil
|
|
state.anitype = nil
|
|
end
|
|
|
|
local function set_osd(res_x, res_y, text, z)
|
|
if state.osd.res_x == res_x and
|
|
state.osd.res_y == res_y and
|
|
state.osd.data == text then
|
|
return
|
|
end
|
|
state.osd.res_x = res_x
|
|
state.osd.res_y = res_y
|
|
state.osd.data = text
|
|
state.osd.z = z
|
|
state.osd:update()
|
|
end
|
|
|
|
local function set_time_styles(timetotal_changed, timems_changed)
|
|
if timetotal_changed then
|
|
state.tc_right_rem = not user_opts.timetotal
|
|
end
|
|
if timems_changed then
|
|
state.tc_ms = user_opts.timems
|
|
end
|
|
end
|
|
|
|
-- scale factor for translating between real and virtual ASS coordinates
|
|
local function get_virt_scale_factor()
|
|
local w, h = mp.get_osd_size()
|
|
if w <= 0 or h <= 0 then
|
|
return 0, 0
|
|
end
|
|
return osc_param.playresx / w, osc_param.playresy / h
|
|
end
|
|
|
|
-- return mouse position in virtual ASS coordinates (playresx/y)
|
|
local function get_virt_mouse_pos()
|
|
if state.mouse_in_window then
|
|
local sx, sy = get_virt_scale_factor()
|
|
local x, y = mp.get_mouse_pos()
|
|
return x * sx, y * sy
|
|
else
|
|
return -1, -1
|
|
end
|
|
end
|
|
|
|
local function set_virt_mouse_area(x0, y0, x1, y1, name)
|
|
local sx, sy = get_virt_scale_factor()
|
|
mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
|
|
end
|
|
|
|
local function scale_value(x0, x1, y0, y1, val)
|
|
local m = (y1 - y0) / (x1 - x0)
|
|
local b = y0 - (m * x0)
|
|
return (m * val) + b
|
|
end
|
|
|
|
-- returns hitbox spanning coordinates (top left, bottom right corner)
|
|
-- according to alignment
|
|
local function get_hitbox_coords(x, y, an, w, h)
|
|
local alignments = {
|
|
[1] = function () return x, y-h, x+w, y end,
|
|
[2] = function () return x-(w/2), y-h, x+(w/2), y end,
|
|
[3] = function () return x-w, y-h, x, y end,
|
|
|
|
[4] = function () return x, y-(h/2), x+w, y+(h/2) end,
|
|
[5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
|
|
[6] = function () return x-w, y-(h/2), x, y+(h/2) end,
|
|
|
|
[7] = function () return x, y, x+w, y+h end,
|
|
[8] = function () return x-(w/2), y, x+(w/2), y+h end,
|
|
[9] = function () return x-w, y, x, y+h end,
|
|
}
|
|
|
|
return alignments[an]()
|
|
end
|
|
|
|
local function get_hitbox_coords_geo(geometry)
|
|
return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
|
|
geometry.w, geometry.h)
|
|
end
|
|
|
|
local function get_element_hitbox(element)
|
|
return element.hitbox.x1, element.hitbox.y1,
|
|
element.hitbox.x2, element.hitbox.y2
|
|
end
|
|
|
|
local function mouse_hit_coords(bX1, bY1, bX2, bY2)
|
|
local mX, mY = get_virt_mouse_pos()
|
|
return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
|
|
end
|
|
|
|
local function mouse_hit(element)
|
|
return mouse_hit_coords(get_element_hitbox(element))
|
|
end
|
|
|
|
local function limit_range(min, max, val)
|
|
if val > max then
|
|
val = max
|
|
elseif val < min then
|
|
val = min
|
|
end
|
|
return val
|
|
end
|
|
|
|
-- translate value into element coordinates
|
|
local function get_slider_ele_pos_for(element, val)
|
|
local ele_pos = scale_value(
|
|
element.slider.min.value, element.slider.max.value,
|
|
element.slider.min.ele_pos, element.slider.max.ele_pos,
|
|
val)
|
|
|
|
return limit_range(
|
|
element.slider.min.ele_pos, element.slider.max.ele_pos,
|
|
ele_pos)
|
|
end
|
|
|
|
-- translates global (mouse) coordinates to value
|
|
local function get_slider_value_at(element, glob_pos)
|
|
if element then
|
|
local val = scale_value(
|
|
element.slider.min.glob_pos, element.slider.max.glob_pos,
|
|
element.slider.min.value, element.slider.max.value,
|
|
glob_pos)
|
|
|
|
return limit_range(
|
|
element.slider.min.value, element.slider.max.value,
|
|
val)
|
|
end
|
|
-- fall back incase of loading errors
|
|
return 0
|
|
end
|
|
|
|
-- get value at current mouse position
|
|
local function get_slider_value(element)
|
|
return get_slider_value_at(element, get_virt_mouse_pos())
|
|
end
|
|
|
|
-- multiplies two alpha values, formular can probably be improved
|
|
local function mult_alpha(alphaA, alphaB)
|
|
return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
|
|
end
|
|
|
|
local function add_area(name, x1, y1, x2, y2)
|
|
-- create area if needed
|
|
if osc_param.areas[name] == nil then
|
|
osc_param.areas[name] = {}
|
|
end
|
|
table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
|
|
end
|
|
|
|
local function ass_append_alpha(ass, alpha, modifier, inverse)
|
|
local ar = {}
|
|
|
|
for ai, av in pairs(alpha) do
|
|
av = mult_alpha(av, modifier)
|
|
if state.animation then
|
|
local animpos = state.animation
|
|
if inverse then
|
|
animpos = 255 - animpos
|
|
end
|
|
av = mult_alpha(av, animpos)
|
|
end
|
|
ar[ai] = av
|
|
end
|
|
|
|
ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
|
|
ar[1], ar[2], ar[3], ar[4]))
|
|
end
|
|
|
|
local function ass_draw_cir_cw(ass, x, y, r)
|
|
ass:round_rect_cw(x-r, y-r, x+r, y+r, r)
|
|
end
|
|
|
|
local function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
|
|
if hexagon then
|
|
ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
|
|
else
|
|
ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
|
|
end
|
|
end
|
|
|
|
local function get_hidetimeout()
|
|
if user_opts.visibility == "always" then
|
|
return -1 -- disable autohide
|
|
end
|
|
return user_opts.hidetimeout
|
|
end
|
|
|
|
local function get_touchtimeout()
|
|
if state.touchtime == nil then
|
|
return 0
|
|
end
|
|
return state.touchtime + (get_hidetimeout() / 1000) - mp.get_time()
|
|
end
|
|
|
|
local function cache_enabled()
|
|
return state.cache_state and #state.cache_state["seekable-ranges"] > 0
|
|
end
|
|
|
|
local function update_margins()
|
|
local margins = osc_param.video_margins
|
|
|
|
-- Don't use margins if it's visible only temporarily.
|
|
if not state.osc_visible or get_hidetimeout() >= 0 or
|
|
(state.fullscreen and not user_opts.showfullscreen) or
|
|
(not state.fullscreen and not user_opts.showwindowed)
|
|
then
|
|
margins = {l = 0, r = 0, t = 0, b = 0}
|
|
end
|
|
|
|
mp.set_property_native("user-data/osc/margins", margins)
|
|
end
|
|
|
|
local tick
|
|
-- Request that tick() is called (which typically re-renders the OSC).
|
|
-- The tick is then either executed immediately, or rate-limited if it was
|
|
-- called a small time ago.
|
|
local function request_tick()
|
|
if state.tick_timer == nil then
|
|
state.tick_timer = mp.add_timeout(0, tick)
|
|
end
|
|
|
|
if not state.tick_timer:is_enabled() then
|
|
local now = mp.get_time()
|
|
local timeout = tick_delay - (now - state.tick_last_time)
|
|
if timeout < 0 then
|
|
timeout = 0
|
|
end
|
|
state.tick_timer.timeout = timeout
|
|
state.tick_timer:resume()
|
|
end
|
|
end
|
|
|
|
local function request_init()
|
|
state.initREQ = true
|
|
request_tick()
|
|
end
|
|
|
|
-- Like request_init(), but also request an immediate update
|
|
local function request_init_resize()
|
|
request_init()
|
|
-- ensure immediate update
|
|
state.tick_timer:kill()
|
|
state.tick_timer.timeout = 0
|
|
state.tick_timer:resume()
|
|
end
|
|
|
|
local function render_wipe()
|
|
msg.trace("render_wipe()")
|
|
state.osd.data = "" -- allows set_osd to immediately update on enable
|
|
state.osd:remove()
|
|
end
|
|
|
|
--
|
|
-- Tracklist Management
|
|
--
|
|
|
|
-- updates the OSC internal playlists, should be run each time the track-layout changes
|
|
local function update_tracklist()
|
|
audio_track_count, sub_track_count = 0, 0
|
|
|
|
for _, track in pairs(mp.get_property_native("track-list")) do
|
|
if track.type == "audio" then
|
|
audio_track_count = audio_track_count + 1
|
|
elseif track.type == "sub" then
|
|
sub_track_count = sub_track_count + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- convert slider_pos to logarithmic depending on volume_control user_opts
|
|
local function set_volume(slider_pos)
|
|
local volume = slider_pos
|
|
if user_opts.volume_control_type == "logarithmic" then
|
|
volume = slider_pos^2 / 100
|
|
end
|
|
return math.floor(volume)
|
|
end
|
|
|
|
-- WindowControl helpers
|
|
local function window_controls_enabled()
|
|
local val = user_opts.window_top_bar
|
|
if val == "auto" then
|
|
return not (state.border and state.title_bar) or state.fullscreen
|
|
else
|
|
return val == "yes"
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Element Management
|
|
--
|
|
local elements = {}
|
|
|
|
local function prepare_elements()
|
|
-- remove elements without layout or invisible
|
|
local elements2 = {}
|
|
for _, element in pairs(elements) do
|
|
if element.layout ~= nil and element.visible then
|
|
table.insert(elements2, element)
|
|
end
|
|
end
|
|
elements = elements2
|
|
|
|
local function elem_compare (a, b)
|
|
return a.layout.layer < b.layout.layer
|
|
end
|
|
|
|
table.sort(elements, elem_compare)
|
|
|
|
for _,element in pairs(elements) do
|
|
|
|
local elem_geo = element.layout.geometry
|
|
|
|
-- Calculate the hitbox
|
|
local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
|
|
element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
|
|
|
|
local style_ass = assdraw.ass_new()
|
|
|
|
-- prepare static elements
|
|
style_ass:append("{}") -- hack to troll new_event into inserting a \n
|
|
style_ass:new_event()
|
|
style_ass:pos(elem_geo.x, elem_geo.y)
|
|
style_ass:an(elem_geo.an)
|
|
style_ass:append(element.layout.style)
|
|
|
|
element.style_ass = style_ass
|
|
|
|
local static_ass = assdraw.ass_new()
|
|
|
|
if element.type == "box" then
|
|
--draw box
|
|
static_ass:draw_start()
|
|
ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
|
|
element.layout.box.radius, element.layout.box.hexagon)
|
|
static_ass:draw_stop()
|
|
|
|
elseif element.type == "slider" then
|
|
--draw static slider parts
|
|
local slider_lo = element.layout.slider
|
|
-- calculate positions of min and max points
|
|
element.slider.min.ele_pos = user_opts.seek_handle_size > 0 and (user_opts.seek_handle_size * elem_geo.h / 2) or slider_lo.border
|
|
element.slider.max.ele_pos = elem_geo.w - element.slider.min.ele_pos
|
|
element.slider.min.glob_pos = element.hitbox.x1 + element.slider.min.ele_pos
|
|
element.slider.max.glob_pos = element.hitbox.x1 + element.slider.max.ele_pos
|
|
|
|
static_ass:draw_start()
|
|
-- a hack which prepares the whole slider area to allow center placements such like an=5
|
|
static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h)
|
|
static_ass:rect_ccw(0, 0, elem_geo.w, elem_geo.h)
|
|
-- marker nibbles
|
|
if element.slider.markerF ~= nil and slider_lo.gap > 0 then
|
|
local markers = element.slider.markerF()
|
|
for _,marker in pairs(markers) do
|
|
if marker >= element.slider.min.value and
|
|
marker <= element.slider.max.value then
|
|
local s = get_slider_ele_pos_for(element, marker)
|
|
if slider_lo.gap > 5 then -- draw triangles / bars
|
|
local bar_h = 3 -- for "bar" and "single-bar" only
|
|
--top
|
|
if slider_lo.nibbles_top then
|
|
if slider_lo.nibbles_style == "triangle" then
|
|
static_ass:move_to(s - 3, slider_lo.gap - 5)
|
|
static_ass:line_to(s + 3, slider_lo.gap - 5)
|
|
static_ass:line_to(s, slider_lo.gap - 1)
|
|
elseif slider_lo.nibbles_style == "bar" then
|
|
static_ass:rect_cw(s - 1, slider_lo.gap - bar_h, s + 1, slider_lo.gap);
|
|
else
|
|
static_ass:rect_cw(s - 1, slider_lo.gap - bar_h, s + 1, elem_geo.h - slider_lo.gap);
|
|
end
|
|
end
|
|
--bottom
|
|
if slider_lo.nibbles_bottom then
|
|
if slider_lo.nibbles_style == "triangle" then
|
|
static_ass:move_to(s - 3, elem_geo.h - slider_lo.gap + 5)
|
|
static_ass:line_to(s, elem_geo.h - slider_lo.gap + 1)
|
|
static_ass:line_to(s + 3, elem_geo.h - slider_lo.gap + 5)
|
|
elseif slider_lo.nibbles_style == "bar" then
|
|
static_ass:rect_cw(s - 1, elem_geo.h - slider_lo.gap, s + 1, elem_geo.h - slider_lo.gap + bar_h);
|
|
else
|
|
static_ass:rect_cw(s - 1, slider_lo.gap, s + 1, elem_geo.h - slider_lo.gap + bar_h);
|
|
end
|
|
end
|
|
else -- draw 2x1px nibbles
|
|
--top
|
|
if slider_lo.nibbles_top then
|
|
static_ass:rect_cw(s - 1, 0, s + 1, slider_lo.gap);
|
|
end
|
|
--bottom
|
|
if slider_lo.nibbles_bottom then
|
|
static_ass:rect_cw(s - 1, elem_geo.h - slider_lo.gap, s + 1, elem_geo.h);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
element.static_ass = static_ass
|
|
|
|
-- if the element is supposed to be disabled,
|
|
-- style it accordingly and kill the eventresponders
|
|
if not element.enabled then
|
|
element.layout.alpha[1] = 215
|
|
if not (element.name == "sub_track" or element.name == "audio_track" or element.name == "tog_playlist") then -- keep these to display tooltips
|
|
element.eventresponder = nil
|
|
end
|
|
end
|
|
|
|
-- gray out the element if it is toggled off
|
|
if element.off then
|
|
element.layout.alpha[1] = 100
|
|
end
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Element Rendering
|
|
--
|
|
|
|
-- returns nil or a chapter element from the native property chapter-list
|
|
local function get_chapter(possec)
|
|
local cl = state.chapter_list -- sorted, get latest before possec, if any
|
|
|
|
for n=#cl,1,-1 do
|
|
if possec >= cl[n].time then
|
|
return cl[n]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draws a handle on the seekbar according to user_opts
|
|
-- Returns handle position and radius
|
|
local function draw_seekbar_handle(element, elem_ass, override_alpha)
|
|
local pos = element.slider.posF()
|
|
if not pos then
|
|
return 0, 0
|
|
end
|
|
local display_handle = user_opts.seek_handle_size > 0
|
|
local elem_geo = element.layout.geometry
|
|
local rh = display_handle and (user_opts.seek_handle_size * elem_geo.h / 2) or 0 -- handle radius
|
|
local xp = get_slider_ele_pos_for(element, pos) -- handle position
|
|
local handle_hovered = mouse_hit_coords(element.hitbox.x1 + xp - rh, element.hitbox.y1 + elem_geo.h / 2 - rh, element.hitbox.x1 + xp + rh, element.hitbox.y1 + elem_geo.h / 2 + rh) and element.enabled
|
|
|
|
if display_handle then
|
|
-- Apply size hover_effect only if hovering over the handle
|
|
if handle_hovered and user_opts.hover_effect_for_sliders then
|
|
if contains(user_opts.hover_effect, "size") then
|
|
rh = rh * (user_opts.hover_button_size / 100)
|
|
end
|
|
end
|
|
|
|
ass_draw_cir_cw(elem_ass, xp, elem_geo.h / 2, rh)
|
|
|
|
if user_opts.hover_effect_for_sliders then
|
|
elem_ass:draw_stop()
|
|
elem_ass:merge(element.style_ass)
|
|
ass_append_alpha(elem_ass, element.layout.alpha, override_alpha or 0)
|
|
elem_ass:merge(element.static_ass)
|
|
end
|
|
|
|
return xp, rh
|
|
end
|
|
return xp, 0
|
|
end
|
|
|
|
-- Draws seekbar ranges according to user_opts
|
|
local function draw_seekbar_ranges(element, elem_ass, xp, rh, override_alpha)
|
|
local handle = xp and rh
|
|
xp = xp or 0
|
|
rh = rh or 0
|
|
local slider_lo = element.layout.slider
|
|
local elem_geo = element.layout.geometry
|
|
local seekRanges = element.slider.seekRangesF()
|
|
if not seekRanges then
|
|
return
|
|
end
|
|
elem_ass:draw_stop()
|
|
elem_ass:merge(element.style_ass)
|
|
ass_append_alpha(elem_ass, element.layout.alpha, override_alpha or user_opts.seekrangealpha)
|
|
elem_ass:append("{\\1cH&" .. osc_color_convert(user_opts.seekbar_cache_color) .. "&}")
|
|
elem_ass:merge(element.static_ass)
|
|
|
|
for _, range in pairs(seekRanges) do
|
|
local pstart = math.max(0, get_slider_ele_pos_for(element, range["start"]) - slider_lo.gap)
|
|
local pend = math.min(elem_geo.w, get_slider_ele_pos_for(element, range["end"]) + slider_lo.gap)
|
|
|
|
if handle and (pstart < xp + rh and pend > xp - rh) then
|
|
if pstart < xp - rh then
|
|
elem_ass:rect_cw(pstart, slider_lo.gap, xp - rh, elem_geo.h - slider_lo.gap)
|
|
end
|
|
pstart = xp + rh
|
|
end
|
|
|
|
if pend > pstart then
|
|
elem_ass:rect_cw(pstart, slider_lo.gap, pend, elem_geo.h - slider_lo.gap)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw seekbar progress more accurately
|
|
local function draw_seekbar_progress(element, elem_ass)
|
|
local pos = element.slider.posF()
|
|
if not pos then
|
|
return
|
|
end
|
|
local xp = get_slider_ele_pos_for(element, pos)
|
|
local slider_lo = element.layout.slider
|
|
local elem_geo = element.layout.geometry
|
|
elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap)
|
|
end
|
|
|
|
local function render_elements(master_ass)
|
|
-- when the slider is dragged or hovered and we have a target chapter name
|
|
-- then we use it instead of the normal title. we calculate it before the
|
|
-- render iterations because the title may be rendered before the slider.
|
|
state.forced_title = nil
|
|
|
|
-- disable displaying chapter name in title when thumbfast is available
|
|
-- because thumbfast will render it above the thumbnail instead
|
|
if thumbfast.disabled then
|
|
local se, ae = state.slider_element, elements[state.active_element]
|
|
if user_opts.chapter_fmt ~= "no" and state.touchingprogressbar then
|
|
local dur = mp.get_property_number("duration", 0)
|
|
if dur > 0 then
|
|
local ch = get_chapter(state.sliderpos * dur / 100)
|
|
if ch and ch.title and ch.title ~= "" then
|
|
state.forced_title = string.format(user_opts.chapter_fmt, ch.title)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
state.touchingprogressbar = false
|
|
|
|
for n=1, #elements do
|
|
local element = elements[n]
|
|
local style_ass = assdraw.ass_new()
|
|
style_ass:merge(element.style_ass)
|
|
ass_append_alpha(style_ass, element.layout.alpha, 0)
|
|
|
|
if element.eventresponder and (state.active_element == n) then
|
|
-- run render event functions
|
|
if element.eventresponder.render ~= nil then
|
|
element.eventresponder.render(element)
|
|
end
|
|
if mouse_hit(element) then
|
|
-- mouse down styling
|
|
if element.styledown then
|
|
style_ass:append(osc_styles.element_down)
|
|
end
|
|
if element.softrepeat and state.mouse_down_counter >= 15
|
|
and state.mouse_down_counter % 5 == 0 then
|
|
|
|
element.eventresponder[state.active_event_source.."_down"](element)
|
|
end
|
|
state.mouse_down_counter = state.mouse_down_counter + 1
|
|
end
|
|
end
|
|
|
|
local elem_ass = assdraw.ass_new()
|
|
elem_ass:merge(style_ass)
|
|
|
|
if element.type ~= "button" then
|
|
elem_ass:merge(element.static_ass)
|
|
end
|
|
|
|
if element.type == "slider" then
|
|
if element.name ~= "persistentseekbar" then
|
|
local slider_lo = element.layout.slider
|
|
local elem_geo = element.layout.geometry
|
|
local s_min = element.slider.min.value
|
|
local s_max = element.slider.max.value
|
|
|
|
local xp, rh = draw_seekbar_handle(element, elem_ass) -- handle posistion, handle radius
|
|
draw_seekbar_progress(element, elem_ass)
|
|
draw_seekbar_ranges(element, elem_ass, xp, rh)
|
|
|
|
elem_ass:draw_stop()
|
|
|
|
-- add tooltip
|
|
if element.slider.tooltipF ~= nil and element.enabled then
|
|
local force_seek_tooltip = user_opts.force_seek_tooltip
|
|
and element.name == "seekbar"
|
|
and element.eventresponder["mbtn_left_down"]
|
|
and element.state.mbtnleft
|
|
and state.mouse_down_counter > 0
|
|
and state.playing_and_seeking
|
|
if mouse_hit(element) or force_seek_tooltip then
|
|
local sliderpos = get_slider_value(element)
|
|
local tooltiplabel = element.slider.tooltipF(sliderpos)
|
|
local an = slider_lo.tooltip_an
|
|
local ty
|
|
if an == 2 then
|
|
ty = element.hitbox.y1 - user_opts.seek_hover_tooltip_h_offset
|
|
else
|
|
ty = element.hitbox.y1 + elem_geo.h / 2 - user_opts.seek_hover_tooltip_h_offset
|
|
end
|
|
|
|
local tx = get_virt_mouse_pos()
|
|
if slider_lo.adjust_tooltip then
|
|
if an == 2 then
|
|
if sliderpos < (s_min + 3) then
|
|
an = an - 1
|
|
elseif sliderpos > (s_max - 3) then
|
|
an = an + 1
|
|
end
|
|
elseif (sliderpos > (s_max+s_min)/2) then
|
|
an = an + 1
|
|
tx = tx - 5
|
|
else
|
|
an = an - 1
|
|
tx = tx + 10
|
|
end
|
|
end
|
|
|
|
if element.name == "seekbar" then
|
|
state.sliderpos = sliderpos
|
|
end
|
|
|
|
-- chapter title tooltip on show_title=false and no thumbfast
|
|
-- add hovered chapter title above time code tooltip on seekbar hover
|
|
if thumbfast.disabled and not user_opts.show_title and not user_opts.show_chapter_title then
|
|
local osd_w = mp.get_property_number("osd-width")
|
|
local r_w, r_h = get_virt_scale_factor()
|
|
if osd_w then
|
|
if user_opts.chapter_fmt ~= "no" and state.touchingprogressbar then
|
|
local dur = mp.get_property_number("duration", 0)
|
|
if dur > 0 then
|
|
local ch = get_chapter(state.sliderpos * dur / 100)
|
|
if ch and ch.title and ch.title ~= "" then
|
|
local titleX = math.min(osd_w - (50 / r_w), math.max((60 / r_w), tx / r_w))
|
|
local titleY = ty - (user_opts.time_font_size * 1.3)
|
|
|
|
elem_ass:new_event()
|
|
elem_ass:pos(titleX * r_w, titleY)
|
|
elem_ass:an(2)
|
|
elem_ass:append(slider_lo.tooltip_style)
|
|
ass_append_alpha(elem_ass, slider_lo.alpha, 0)
|
|
elem_ass:append(string.format(user_opts.chapter_fmt, ch.title))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- thumbfast
|
|
elseif element.thumbnailable and not thumbfast.disabled then
|
|
local osd_w = mp.get_property_number("osd-width")
|
|
local r_w, r_h = get_virt_scale_factor()
|
|
|
|
if osd_w then
|
|
local hover_sec = 0
|
|
if mp.get_property_number("duration") then hover_sec = mp.get_property_number("duration") * sliderpos / 100 end
|
|
local thumbPad = user_opts.thumbnail_border
|
|
local thumbMarginX = 18 / r_w
|
|
local thumbMarginY = user_opts.time_font_size + thumbPad + 2 / r_h
|
|
local thumbX = math.min(osd_w - thumbfast.width - thumbMarginX, math.max(thumbMarginX, tx / r_w - thumbfast.width / 2))
|
|
local thumbY = (ty - thumbMarginY) / r_h - thumbfast.height
|
|
|
|
thumbX = math.floor(thumbX + 0.5)
|
|
thumbY = math.floor(thumbY + 0.5)
|
|
|
|
if state.anitype == nil then
|
|
elem_ass:new_event()
|
|
elem_ass:append("{\\rDefault}")
|
|
elem_ass:pos(thumbX * r_w, ty - thumbMarginY - thumbfast.height * r_h)
|
|
elem_ass:an(7)
|
|
elem_ass:append(osc_styles.thumbnail)
|
|
elem_ass:draw_start()
|
|
if user_opts.thumbnail_border_radius and user_opts.thumbnail_border_radius > 0 then
|
|
elem_ass:round_rect_cw(-thumbPad * r_w, -thumbPad * r_h, (thumbfast.width + thumbPad) * r_w, (thumbfast.height + thumbPad) * r_h, user_opts.thumbnail_border_radius)
|
|
else
|
|
elem_ass:rect_cw(-thumbPad * r_w, -thumbPad * r_h, (thumbfast.width + thumbPad) * r_w, (thumbfast.height + thumbPad) * r_h)
|
|
end
|
|
elem_ass:draw_stop()
|
|
|
|
-- force tooltip to be centered on the thumb, even at far left/right of screen
|
|
tx = (thumbX + thumbfast.width / 2) * r_w
|
|
an = 2
|
|
|
|
mp.commandv("script-message-to", "thumbfast", "thumb", hover_sec, thumbX, thumbY)
|
|
end
|
|
|
|
-- chapter title tooltip
|
|
local se, ae = state.slider_element, elements[state.active_element]
|
|
if user_opts.chapter_fmt ~= "no" and state.touchingprogressbar then
|
|
local dur = mp.get_property_number("duration", 0)
|
|
if dur > 0 then
|
|
local ch = get_chapter(state.sliderpos * dur / 100)
|
|
if ch and ch.title and ch.title ~= "" then
|
|
elem_ass:new_event()
|
|
elem_ass:pos((thumbX + thumbfast.width / 2) * r_w, thumbY * r_h - user_opts.time_font_size / 2)
|
|
elem_ass:an(an)
|
|
elem_ass:append(slider_lo.tooltip_style)
|
|
ass_append_alpha(elem_ass, slider_lo.alpha, 0)
|
|
elem_ass:append(string.format(user_opts.chapter_fmt, ch.title))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- tooltip label
|
|
elem_ass:new_event()
|
|
elem_ass:pos(tx, ty)
|
|
elem_ass:an(an)
|
|
elem_ass:append(slider_lo.tooltip_style)
|
|
ass_append_alpha(elem_ass, slider_lo.alpha, 0)
|
|
elem_ass:append(tooltiplabel)
|
|
elseif element.thumbnailable and thumbfast.available then
|
|
mp.commandv("script-message-to", "thumbfast", "clear")
|
|
end
|
|
end
|
|
end
|
|
|
|
elseif element.type == "button" then
|
|
local buttontext
|
|
if type(element.content) == "function" then
|
|
buttontext = element.content() -- function objects
|
|
elseif element.content ~= nil then
|
|
buttontext = element.content -- text objects
|
|
end
|
|
|
|
local maxchars = element.layout.button.maxchars
|
|
if maxchars ~= nil and #buttontext > maxchars then
|
|
local max_ratio = 1.25 -- up to 25% more chars while shrinking
|
|
local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
|
|
if #buttontext > limit then
|
|
while (#buttontext > limit) do
|
|
buttontext = buttontext:gsub(".[\128-\191]*$", "")
|
|
end
|
|
buttontext = buttontext .. "..."
|
|
end
|
|
buttontext = string.format("{\\fscx%f}",
|
|
(maxchars/#buttontext)*100) .. buttontext
|
|
end
|
|
|
|
-- add hover effects
|
|
local button_lo = element.layout.button
|
|
local is_clickable = element.eventresponder and (
|
|
element.eventresponder["mbtn_left_down"] ~= nil or
|
|
element.eventresponder["mbtn_left_up"] ~= nil
|
|
)
|
|
local hovered = mouse_hit(element) and is_clickable and element.enabled and state.mouse_down_counter == 0
|
|
local hoverstyle = button_lo.hoverstyle
|
|
if hovered and (contains(user_opts.hover_effect, "size") or contains(user_opts.hover_effect, "color")) then
|
|
-- remove font scale tags for these elements, it looks out of place
|
|
if element.name == "title" or element.name == "time_codes" or element.name == "chapter_title" or element.name == "cache_info" then
|
|
hoverstyle = hoverstyle:gsub("\\fscx%d+\\fscy%d+", "")
|
|
end
|
|
elem_ass:append(hoverstyle .. buttontext)
|
|
else
|
|
elem_ass:append(buttontext)
|
|
end
|
|
|
|
-- apply blur effect if "glow" is in hover effects
|
|
if hovered and contains(user_opts.hover_effect, "glow") then
|
|
local shadow_ass = assdraw.ass_new()
|
|
shadow_ass:merge(style_ass)
|
|
shadow_ass:append("{\\blur" .. user_opts.button_glow_amount .. "}" .. hoverstyle .. buttontext)
|
|
elem_ass:merge(shadow_ass)
|
|
end
|
|
|
|
-- add tooltip for button elements
|
|
if element.tooltipF ~= nil and (user_opts.tooltips_for_disabled_elements or element.enabled) then
|
|
if mouse_hit(element) then
|
|
local tooltiplabel = element.tooltipF
|
|
local an = 1
|
|
local ty = element.hitbox.y1 - user_opts.tooltip_height_offset
|
|
local tx = get_virt_mouse_pos()
|
|
|
|
if ty < osc_param.playresy / 2 then
|
|
ty = element.hitbox.y2 - user_opts.tooltip_height_offset
|
|
an = 7
|
|
end
|
|
|
|
-- tooltip label
|
|
if element.enabled then
|
|
if type(element.tooltipF) == "function" then
|
|
tooltiplabel = element.tooltipF()
|
|
else
|
|
tooltiplabel = element.tooltipF
|
|
end
|
|
else
|
|
tooltiplabel = element.nothingavailable
|
|
end
|
|
|
|
if tx > osc_param.playresx / 2 then -- move tooltip to left side of mouse cursor
|
|
tx = tx - string.len(tooltiplabel) * user_opts.tooltip_left_offset
|
|
end
|
|
|
|
elem_ass:new_event()
|
|
elem_ass:append("{\\rDefault}")
|
|
elem_ass:pos(tx, ty)
|
|
elem_ass:an(an)
|
|
elem_ass:append(element.tooltip_style)
|
|
elem_ass:append(tooltiplabel)
|
|
end
|
|
end
|
|
end
|
|
|
|
master_ass:merge(elem_ass)
|
|
end
|
|
end
|
|
|
|
local function render_persistentprogressbar(master_ass)
|
|
for n=1, #elements do
|
|
local element = elements[n]
|
|
if element.name == "persistentseekbar" then
|
|
local style_ass = assdraw.ass_new()
|
|
style_ass:merge(element.style_ass)
|
|
if state.animation or not state.osc_visible then
|
|
ass_append_alpha(style_ass, element.layout.alpha, 0, true)
|
|
|
|
local elem_ass = assdraw.ass_new()
|
|
elem_ass:merge(style_ass)
|
|
if element.type ~= "button" then
|
|
elem_ass:merge(element.static_ass)
|
|
end
|
|
|
|
-- draw pos marker
|
|
draw_seekbar_progress(element, elem_ass)
|
|
|
|
if user_opts.persistentbuffer then
|
|
draw_seekbar_ranges(element, elem_ass, nil, nil)
|
|
end
|
|
|
|
elem_ass:draw_stop()
|
|
master_ass:merge(elem_ass)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Initialisation and Layout
|
|
--
|
|
local function is_url(s)
|
|
if not s then
|
|
user_opts.download_button = false
|
|
return false
|
|
end
|
|
|
|
local url_pattern = "^[%w]+://[%w%.%-_]+%.[%a]+[-%w%.%-%_/?&=]*"
|
|
return string.match(s, url_pattern) ~= nil
|
|
end
|
|
|
|
local function exec_filesize(args)
|
|
for i = #args, 1, -1 do
|
|
if args[i] == nil or args[i] == "" then
|
|
table.remove(args, i)
|
|
end
|
|
end
|
|
|
|
mp.command_native_async({
|
|
name = "subprocess",
|
|
args = args,
|
|
capture_stdout = true,
|
|
capture_stderr = true
|
|
}, function(res, val)
|
|
local fileSizeString = val.stdout
|
|
state.file_size_bytes = tonumber(fileSizeString)
|
|
|
|
if state.file_size_bytes then
|
|
state.file_size_normalized = utils.format_bytes_humanized(state.file_size_bytes)
|
|
msg.info("Download size: " .. state.file_size_normalized)
|
|
else
|
|
local fs_prop = mp.get_property_osd("file-size")
|
|
|
|
if fs_prop and fs_prop ~= "" then
|
|
state.file_size_normalized = fs_prop
|
|
msg.info("Download size: " .. fs_prop)
|
|
else
|
|
state.file_size_normalized = "Unknown"
|
|
msg.info("Unable to retrieve file size.")
|
|
end
|
|
end
|
|
|
|
request_tick()
|
|
end)
|
|
end
|
|
|
|
local function download_done(success, result, error)
|
|
if success then
|
|
local download_path = mp.command_native({"expand-path", user_opts.download_path})
|
|
mp.command("show-text 'Download saved to " .. download_path .. "'")
|
|
state.downloaded_once = true
|
|
msg.info("Download completed")
|
|
else
|
|
mp.command("show-text 'Download failed - " .. (error or "Unknown error") .. "'")
|
|
msg.info("Download failed")
|
|
end
|
|
state.downloading = false
|
|
end
|
|
|
|
local function exec(args, callback)
|
|
for i = #args, 1, -1 do
|
|
if args[i] == nil or args[i] == "" then
|
|
table.remove(args, i)
|
|
end
|
|
end
|
|
|
|
msg.info("Executing command: " .. table.concat(args, " "))
|
|
|
|
local ret = mp.command_native_async({
|
|
name = "subprocess",
|
|
args = args,
|
|
capture_stdout = true,
|
|
capture_stderr = true
|
|
}, callback)
|
|
|
|
return ret and ret.status or nil
|
|
end
|
|
|
|
local function check_path_url()
|
|
state.is_URL = false
|
|
state.downloading = false
|
|
|
|
local path = mp.get_property("path")
|
|
if not path then return nil end
|
|
|
|
if string.find(path, "https://") then
|
|
path = string.gsub(path, "ytdl://", "") -- Remove "ytdl://" prefix
|
|
else
|
|
path = string.gsub(path, "ytdl://", "https://") -- Replace "ytdl://" with "https://"
|
|
end
|
|
|
|
-- use current or default ytdl-format
|
|
local mpv_ytdl = mp.get_property("file-local-options/ytdl-format") or mp.get_property("ytdl-format") or ""
|
|
local ytdl_format = (mpv_ytdl and mpv_ytdl ~= "") and "-f " .. mpv_ytdl or "-f " .. "bestvideo+bestaudio/best"
|
|
|
|
if is_url(path) then
|
|
state.is_URL = true
|
|
state.url_path = path
|
|
msg.info("URL detected.")
|
|
|
|
if user_opts.download_button then
|
|
msg.info("Fetching file size...")
|
|
local command = {
|
|
"yt-dlp",
|
|
state.is_image and "" or ytdl_format,
|
|
"--no-download",
|
|
"-O",
|
|
"%(filesize,filesize_approx)s", -- Fetch file size or approximate size
|
|
path
|
|
}
|
|
exec_filesize(command)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function new_element(name, type)
|
|
elements[name] = {}
|
|
elements[name].type = type
|
|
elements[name].name = name
|
|
|
|
-- add default stuff
|
|
elements[name].eventresponder = {}
|
|
elements[name].visible = true
|
|
elements[name].enabled = true
|
|
elements[name].softrepeat = false
|
|
elements[name].styledown = (type == "button")
|
|
elements[name].state = {}
|
|
|
|
if type == "slider" then
|
|
elements[name].slider = {min = {value = 0}, max = {value = 100}}
|
|
elements[name].thumbnailable = false
|
|
end
|
|
|
|
return elements[name]
|
|
end
|
|
|
|
local function add_layout(name)
|
|
if elements[name] ~= nil then
|
|
-- new layout
|
|
elements[name].layout = {}
|
|
|
|
-- set layout defaults
|
|
elements[name].layout.layer = 50
|
|
elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
|
|
|
|
if elements[name].type == "button" then
|
|
elements[name].layout.button = {
|
|
maxchars = nil,
|
|
hoverstyle = osc_styles.element_hover,
|
|
}
|
|
elseif elements[name].type == "slider" then
|
|
-- slider defaults
|
|
elements[name].layout.slider = {
|
|
border = 1,
|
|
gap = 1,
|
|
nibbles_top = user_opts.nibbles_top,
|
|
nibbles_bottom = user_opts.nibbles_bottom,
|
|
nibbles_style = user_opts.nibbles_style,
|
|
adjust_tooltip = true,
|
|
tooltip_style = "",
|
|
tooltip_an = 2,
|
|
alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
|
|
hoverstyle = osc_styles.element_hover:gsub("\\fscx%d+\\fscy%d+", ""), -- font scales messes with handle positions in werid ways
|
|
}
|
|
elseif elements[name].type == "box" then
|
|
elements[name].layout.box = {radius = 0, hexagon = false}
|
|
end
|
|
|
|
return elements[name].layout
|
|
else
|
|
msg.error("Can't add_layout to element '"..name.."', doesn't exist.")
|
|
end
|
|
end
|
|
|
|
-- Window Controls
|
|
local function window_controls()
|
|
local wc_geo = {
|
|
x = 0,
|
|
y = 50,
|
|
an = 1,
|
|
w = osc_param.playresx,
|
|
h = 50,
|
|
}
|
|
|
|
local lo
|
|
local controlbox_w = window_control_box_width
|
|
local titlebox_w = wc_geo.w - controlbox_w
|
|
local controlbox_left = wc_geo.w - controlbox_w
|
|
local titlebox_left = wc_geo.x
|
|
local titlebox_right = wc_geo.w - controlbox_w
|
|
local button_y = wc_geo.y - (wc_geo.h / 2)
|
|
local first_geo = {x = controlbox_left + 25, y = button_y, an = 5, w = 50, h = wc_geo.h}
|
|
local second_geo = {x = controlbox_left + 75, y = button_y, an = 5, w = 49, h = wc_geo.h}
|
|
local third_geo = {x = controlbox_left + 125, y = button_y, an = 5, w = 50, h = wc_geo.h}
|
|
|
|
-- Window controls
|
|
if user_opts.window_controls then
|
|
-- Close: 🗙
|
|
lo = add_layout("close")
|
|
lo.geometry = third_geo
|
|
lo.style = osc_styles.window_control
|
|
lo.button.hoverstyle = "{\\c&H" .. osc_color_convert(user_opts.windowcontrols_close_hover) .. "&" .. (contains(user_opts.hover_effect, "size") and string.format("\\fscx%s\\fscy%s", user_opts.hover_button_size, user_opts.hover_button_size) or "") .. "}"
|
|
|
|
-- Minimize: 🗕
|
|
lo = add_layout("minimize")
|
|
lo.geometry = first_geo
|
|
lo.style = osc_styles.window_control
|
|
lo.button.hoverstyle = "{\\c&H" .. osc_color_convert(user_opts.windowcontrols_min_hover) .. "&" .. (contains(user_opts.hover_effect, "size") and string.format("\\fscx%s\\fscy%s", user_opts.hover_button_size, user_opts.hover_button_size) or "") .. "}"
|
|
|
|
-- Maximize: 🗖 /🗗
|
|
lo = add_layout("maximize")
|
|
lo.geometry = second_geo
|
|
lo.style = osc_styles.window_control
|
|
lo.button.hoverstyle = "{\\c&H" .. osc_color_convert(user_opts.windowcontrols_max_hover) .. "&" .. (contains(user_opts.hover_effect, "size") and string.format("\\fscx%s\\fscy%s", user_opts.hover_button_size, user_opts.hover_button_size) or "") .. "}"
|
|
|
|
add_area("window-controls", get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an, controlbox_w, wc_geo.h))
|
|
end
|
|
|
|
-- Window Title
|
|
if user_opts.window_title then
|
|
lo = add_layout("windowtitle")
|
|
lo.geometry = {x = 20, y = button_y + 14, an = 1, w = osc_param.playresx - 50, h = wc_geo.h}
|
|
lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", osc_styles.window_title, titlebox_left, wc_geo.y - wc_geo.h, titlebox_right, wc_geo.y + wc_geo.h)
|
|
|
|
add_area("window-controls-title", titlebox_left, 0, titlebox_right, wc_geo.h)
|
|
end
|
|
end
|
|
|
|
--
|
|
-- ModernZ Layout
|
|
--
|
|
|
|
local layouts = {}
|
|
|
|
-- Default layout
|
|
layouts["modern"] = function ()
|
|
local no_title =
|
|
((user_opts.title_mbtn_left_command == "" or user_opts.title_mbtn_left_command == "ignore") and
|
|
(user_opts.title_mbtn_right_command == "" or user_opts.title_mbtn_right_command == "ignore")) or
|
|
not user_opts.show_title
|
|
|
|
local no_chapter =
|
|
((user_opts.chapter_title_mbtn_left_command == "" or user_opts.chapter_title_mbtn_left_command == "ignore") and
|
|
(user_opts.chapter_title_mbtn_right_command == "" or user_opts.chapter_title_mbtn_right_command == "ignore")) or
|
|
not user_opts.show_chapter_title
|
|
|
|
local chapter_index = user_opts.show_chapter_title and mp.get_property_number("chapter", -1) >= 0
|
|
local osc_height_offset = (no_title and user_opts.notitle_osc_h_offset or 0) + ((no_chapter or not chapter_index) and user_opts.nochapter_osc_h_offset or 0)
|
|
|
|
local osc_geo = {
|
|
w = osc_param.playresx,
|
|
h = user_opts.osc_height - osc_height_offset
|
|
}
|
|
|
|
-- update bottom margin
|
|
osc_param.video_margins.b = math.max(user_opts.osc_height, user_opts.fade_alpha) / osc_param.playresy
|
|
|
|
-- origin of the controllers, left/bottom corner
|
|
local posX = 0
|
|
local posY = osc_param.playresy
|
|
|
|
osc_param.areas = {} -- delete areas
|
|
|
|
-- area for active mouse input
|
|
add_area("input", get_hitbox_coords(posX, posY, 1, osc_geo.w, osc_geo.h))
|
|
|
|
-- area for show/hide
|
|
add_area("showhide", 0, 0, osc_param.playresx, osc_param.playresy)
|
|
|
|
-- fetch values
|
|
local osc_w, osc_h = osc_geo.w, osc_geo.h
|
|
|
|
-- Controller Background
|
|
local lo, geo
|
|
|
|
new_element("osc_fade_bg", "box")
|
|
lo = add_layout("osc_fade_bg")
|
|
lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1}
|
|
lo.style = osc_styles.osc_fade_bg
|
|
lo.layer = 10
|
|
lo.alpha[3] = user_opts.fade_transparency_strength
|
|
|
|
local top_titlebar = window_controls_enabled() and (user_opts.window_title or user_opts.window_controls)
|
|
|
|
-- Window bar alpha
|
|
if ((user_opts.window_top_bar == "yes" or not (state.border and state.title_bar)) or state.fullscreen) and top_titlebar then
|
|
new_element("window_bar_alpha_bg", "box")
|
|
lo = add_layout("window_bar_alpha_bg")
|
|
lo.geometry = {x = posX, y = -100, an = 7, w = osc_w, h = -1}
|
|
lo.style = osc_styles.window_fade_bg
|
|
lo.layer = 10
|
|
lo.alpha[3] = user_opts.window_fade_transparency_strength
|
|
end
|
|
|
|
-- Alignment
|
|
local refX = osc_w / 2
|
|
local refY = posY
|
|
|
|
-- Seekbar
|
|
new_element("seekbarbg", "box")
|
|
lo = add_layout("seekbarbg")
|
|
local seekbar_bg_h = 4
|
|
lo.geometry = {x = refX, y = refY - 72, an = 5, w = osc_geo.w - 50, h = seekbar_bg_h}
|
|
lo.layer = 13
|
|
lo.style = osc_styles.seekbar_bg
|
|
lo.alpha[1] = 128
|
|
lo.alpha[3] = 128
|
|
|
|
lo = add_layout("seekbar")
|
|
local seekbar_h = 18
|
|
lo.geometry = {x = refX, y = refY - 72, an = 5, w = osc_geo.w - 50, h = seekbar_h}
|
|
lo.layer = 51
|
|
lo.style = osc_styles.seekbar_fg
|
|
lo.slider.gap = (seekbar_h - seekbar_bg_h) / 2.0
|
|
lo.slider.tooltip_style = osc_styles.tooltip
|
|
lo.slider.tooltip_an = 2
|
|
|
|
if user_opts.persistentprogress or state.persistent_progress_toggle then
|
|
lo = add_layout("persistentseekbar")
|
|
lo.geometry = {x = refX, y = refY, an = 5, w = osc_geo.w, h = user_opts.persistentprogressheight}
|
|
lo.style = osc_styles.seekbar_fg
|
|
lo.slider.gap = (seekbar_h - seekbar_bg_h) / 2.0
|
|
lo.slider.tooltip_an = 0
|
|
end
|
|
|
|
local audio_track = audio_track_count > 0
|
|
local subtitle_track = sub_track_count > 0
|
|
local jump_buttons = user_opts.jump_buttons
|
|
local chapter_skip_buttons = user_opts.chapter_skip_buttons
|
|
local track_nextprev_buttons = user_opts.track_nextprev_buttons
|
|
local fullscreen_button = user_opts.fullscreen_button
|
|
local info_button = user_opts.info_button
|
|
local ontop_button = user_opts.ontop_button
|
|
local screenshot_button = user_opts.screenshot_button
|
|
local loop_button = user_opts.loop_button
|
|
local speed_button = user_opts.speed_button
|
|
local download_button = user_opts.download_button and state.is_URL
|
|
local playlist_button = user_opts.playlist_button and (not user_opts.hide_empty_playlist_button or mp.get_property_number("playlist-count", 0) > 1)
|
|
|
|
local offset = jump_buttons and 60 or 0
|
|
local outeroffset = (chapter_skip_buttons and 0 or 100) + (jump_buttons and 0 or 100)
|
|
|
|
-- OSC title
|
|
geo = {x = 25, y = refY - (chapter_index and user_opts.title_with_chapter_height or user_opts.title_height), an = 1, w = osc_geo.w - 50 - (loop_button and 45 or 0) - (speed_button and 45 or 0), h = user_opts.title_font_size}
|
|
lo = add_layout("title")
|
|
lo.geometry = geo
|
|
lo.style = string.format("%s{\\clip(0,%f,%f,%f)}", osc_styles.title, geo.y - geo.h, geo.x + geo.w, geo.y + geo.h)
|
|
lo.alpha[3] = 0
|
|
|
|
-- Chapter title (above seekbar)
|
|
if user_opts.show_chapter_title then
|
|
lo = add_layout("chapter_title")
|
|
lo.geometry = {x = 26, y = refY - user_opts.chapter_title_height, an = 1, w = osc_geo.w / 2, h = user_opts.chapter_title_font_size}
|
|
lo.style = string.format("%s{\\clip(0,%f,%f,%f)}", osc_styles.chapter_title, geo.y - geo.h, geo.x + geo.w, geo.y + geo.h)
|
|
end
|
|
|
|
-- buttons
|
|
if track_nextprev_buttons then
|
|
lo = add_layout("playlist_prev")
|
|
lo.geometry = {x = refX - (60 + (chapter_skip_buttons and 60 or 0)) - offset, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
if chapter_skip_buttons then
|
|
lo = add_layout("chapter_backward")
|
|
lo.geometry = {x = refX - 60 - offset, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
if jump_buttons then
|
|
lo = add_layout("jump_backward")
|
|
lo.geometry = {x = refX - 60, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = (user_opts.jump_icon_number and icons.jump[user_opts.jump_amount] ~= nil) and osc_styles.control_2 or osc_styles.control_2_flip
|
|
end
|
|
|
|
lo = add_layout("play_pause")
|
|
lo.geometry = {x = refX, y = refY - 35, an = 5, w = 45, h = 28}
|
|
lo.style = osc_styles.control_1
|
|
|
|
if jump_buttons then
|
|
lo = add_layout("jump_forward")
|
|
lo.geometry = {x = refX + 60, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
if chapter_skip_buttons then
|
|
lo = add_layout("chapter_forward")
|
|
lo.geometry = {x = refX + 60 + offset, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
if track_nextprev_buttons then
|
|
lo = add_layout("playlist_next")
|
|
lo.geometry = {x = refX + (60 + (chapter_skip_buttons and 60 or 0)) + offset, y = refY - 35, an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
local start_x = 37
|
|
|
|
-- Playlist
|
|
if playlist_button then
|
|
lo = add_layout("tog_playlist")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset)
|
|
start_x = start_x + 45
|
|
end
|
|
|
|
-- Audio
|
|
if audio_track then
|
|
lo = add_layout("audio_track")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 500 - outeroffset)
|
|
start_x = start_x + 45
|
|
end
|
|
|
|
-- Subtitle
|
|
if subtitle_track then
|
|
lo = add_layout("sub_track")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset)
|
|
start_x = start_x + 45
|
|
end
|
|
|
|
if audio_track then
|
|
-- Volume
|
|
lo = add_layout("vol_ctrl")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset)
|
|
start_x = start_x + 28
|
|
|
|
-- Volumebar
|
|
lo = new_element("volumebarbg", "box")
|
|
lo.visible = (osc_param.playresx >= 1150 - outeroffset) and user_opts.volume_control
|
|
lo = add_layout("volumebarbg")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 4, w = 55, h = 4}
|
|
lo.layer = 13
|
|
lo.alpha[1] = 128
|
|
lo.style = user_opts.volumebar_match_seek_color and osc_styles.seekbar_bg or osc_styles.volumebar_bg
|
|
|
|
lo = add_layout("volumebar")
|
|
lo.geometry = {x = start_x, y = refY - 35, an = 4, w = 55, h = 10}
|
|
lo.style = user_opts.volumebar_match_seek_color and osc_styles.seekbar_fg or osc_styles.volumebar_fg
|
|
lo.slider.gap = 3
|
|
lo.slider.tooltip_style = osc_styles.tooltip
|
|
lo.slider.tooltip_an = 2
|
|
start_x = start_x + 75
|
|
end
|
|
|
|
-- Time codes
|
|
local remsec = mp.get_property_number("playtime-remaining", 0)
|
|
local dur = mp.get_property_number("duration", 0)
|
|
local show_hours = mp.get_property_number("playback-time", 0) >= 3600 or user_opts.time_format ~= "dynamic"
|
|
local show_remhours = (state.tc_right_rem and remsec >= 3600) or (not state.tc_right_rem and dur >= 3600) or user_opts.time_format ~= "dynamic"
|
|
local auto_hide_volbar = (audio_track and user_opts.volume_control) and osc_param.playresx < (user_opts.hide_volume_bar_trigger - outeroffset)
|
|
local time_codes_x = start_x
|
|
- (auto_hide_volbar and 75 or 0) -- window width with audio track and elements
|
|
- (audio_track and not user_opts.volume_control and 115 or 0) -- audio track with no elements
|
|
- (not audio_track and 12 or 0) -- remove extra padding
|
|
local time_codes_width = 80
|
|
+ (state.tc_ms and 50 or 0)
|
|
+ (state.tc_right_rem and 15 or 0)
|
|
+ (show_hours and 20 or 0)
|
|
+ (show_remhours and 20 or 0)
|
|
local narrow_win = osc_param.playresx < (
|
|
user_opts.portrait_window_trigger
|
|
- outeroffset
|
|
- (playlist_button and 0 or 100)
|
|
- (subtitle_track and 0 or 100)
|
|
- (audio_track and 0 or 100)
|
|
)
|
|
lo = add_layout("time_codes")
|
|
lo.geometry = {x = (narrow_win and refX or time_codes_x), y = refY - (narrow_win and user_opts.time_codes_centered_height or user_opts.time_codes_height), an = (narrow_win and 5 or 4), w = time_codes_width, h = user_opts.time_font_size}
|
|
lo.style = osc_styles.time
|
|
|
|
-- Fullscreen/Info/Pin/Screenshot/Loop/Speed
|
|
local end_x = osc_geo.w - 37
|
|
if fullscreen_button then
|
|
lo = add_layout("tog_fullscreen")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 250 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if info_button then
|
|
lo = add_layout("tog_info")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 300 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if ontop_button then
|
|
lo = add_layout("tog_ontop")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 500 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if screenshot_button then
|
|
lo = add_layout("screenshot")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if loop_button then
|
|
lo = add_layout("tog_loop")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset) and loop_button
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if speed_button then
|
|
lo = add_layout("tog_speed")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 600 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
if download_button then
|
|
lo = add_layout("download")
|
|
lo.geometry = {x = end_x, y = refY - 35, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 400 - outeroffset)
|
|
end_x = end_x - 45
|
|
end
|
|
|
|
-- cache info
|
|
if user_opts.cache_info then
|
|
lo = add_layout("cache_info")
|
|
lo.geometry = {x = end_x + 7, y = refY - 35, an = 6, w = (user_opts.cache_info_speed and 70 or 45), h = 24}
|
|
lo.style = osc_styles.cache
|
|
end
|
|
end
|
|
|
|
layouts["modern-image"] = function ()
|
|
local osc_geo = {
|
|
w = osc_param.playresx,
|
|
h = 50
|
|
}
|
|
|
|
-- update bottom margin
|
|
osc_param.video_margins.b = math.max(50, user_opts.fade_alpha) / osc_param.playresy
|
|
|
|
-- origin of the controllers, left/bottom corner
|
|
local posX = 0
|
|
local posY = osc_param.playresy
|
|
|
|
osc_param.areas = {} -- delete areas
|
|
|
|
-- area for active mouse input
|
|
add_area("input", get_hitbox_coords(posX, posY, 1, osc_geo.w, osc_geo.h))
|
|
|
|
-- area for show/hide
|
|
add_area("showhide", 0, 0, osc_param.playresx, osc_param.playresy)
|
|
|
|
-- fetch values
|
|
local osc_w, osc_h = osc_geo.w, osc_geo.h
|
|
|
|
-- Controller Background
|
|
local lo, geo
|
|
|
|
new_element("osc_fade_bg", "box")
|
|
lo = add_layout("osc_fade_bg")
|
|
lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1}
|
|
lo.style = osc_styles.osc_fade_bg
|
|
lo.layer = 10
|
|
lo.alpha[3] = user_opts.fade_transparency_strength
|
|
|
|
local top_titlebar = window_controls_enabled() and (user_opts.window_title or user_opts.window_controls)
|
|
|
|
-- Window bar alpha
|
|
if ((user_opts.window_top_bar == "yes" or not (state.border and state.title_bar)) or state.fullscreen) and top_titlebar then
|
|
new_element("window_bar_alpha_bg", "box")
|
|
lo = add_layout("window_bar_alpha_bg")
|
|
lo.geometry = {x = posX, y = -100, an = 7, w = osc_w, h = -1}
|
|
lo.style = osc_styles.window_fade_bg
|
|
lo.layer = 10
|
|
lo.alpha[3] = user_opts.window_fade_transparency_strength
|
|
end
|
|
|
|
-- Alignment
|
|
local refX = osc_w / 2
|
|
local refY = posY
|
|
|
|
local track_nextprev_buttons = user_opts.track_nextprev_buttons
|
|
local fullscreen_button = user_opts.fullscreen_button
|
|
local info_button = user_opts.info_button
|
|
local ontop_button = user_opts.ontop_button
|
|
local playlist_button = user_opts.playlist_button and (not user_opts.hide_empty_playlist_button or mp.get_property_number("playlist-count", 0) > 1)
|
|
local zoom_control = user_opts.zoom_control
|
|
|
|
-- Playlist
|
|
if playlist_button then
|
|
lo = add_layout("tog_playlist")
|
|
lo.geometry = {x = 25, y = refY - 30, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = osc_param.playresx >= 250
|
|
end
|
|
|
|
if track_nextprev_buttons then
|
|
lo = add_layout("playlist_prev")
|
|
lo.geometry = {x = 55 - (playlist_button and 0 or 25), y = refY - 30 , an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
|
|
lo = add_layout("playlist_next")
|
|
lo.geometry = {x = 80 - (playlist_button and 0 or 25), y = refY - 30 , an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
if zoom_control then
|
|
-- zoom control
|
|
lo = add_layout("zoom_out_icon")
|
|
lo.geometry = {x = 130 - (playlist_button and 0 or 25) - (track_nextprev_buttons and 0 or 70), y = refY - 30 , an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
|
|
lo = new_element("zoom_control_bg", "box")
|
|
lo.visible = osc_param.playresx >= 400 and user_opts.zoom_control
|
|
lo = add_layout("zoom_control_bg")
|
|
lo.geometry = {x = 145 - (playlist_button and 0 or 25) - (track_nextprev_buttons and 0 or 70), y = refY - 30, an = 4, w = 80, h = 4}
|
|
lo.layer = 13
|
|
lo.alpha[1] = 128
|
|
lo.style = osc_styles.volumebar_bg
|
|
|
|
lo = add_layout("zoom_control")
|
|
lo.geometry = {x = 145 - (playlist_button and 0 or 25) - (track_nextprev_buttons and 0 or 70), y = refY - 30, an = 4, w = 80, h = 10}
|
|
lo.style = osc_styles.volumebar_fg
|
|
lo.slider.gap = 3
|
|
lo.slider.tooltip_style = osc_styles.tooltip
|
|
lo.slider.tooltip_an = 2
|
|
|
|
lo = add_layout("zoom_in_icon")
|
|
lo.geometry = {x = 240 - (playlist_button and 0 or 25) - (track_nextprev_buttons and 0 or 70), y = refY - 30 , an = 5, w = 30, h = 24}
|
|
lo.style = osc_styles.control_2
|
|
end
|
|
|
|
-- Fullscreen/Info/Pin/Download
|
|
if fullscreen_button then
|
|
lo = add_layout("tog_fullscreen")
|
|
lo.geometry = {x = osc_geo.w - 37, y = refY - 30, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 250)
|
|
end
|
|
|
|
if info_button then
|
|
lo = add_layout("tog_info")
|
|
lo.geometry = {x = osc_geo.w - 82 + (fullscreen_button and 0 or 45), y = refY - 30, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 300)
|
|
end
|
|
|
|
if ontop_button then
|
|
lo = add_layout("tog_ontop")
|
|
lo.geometry = {x = osc_geo.w - 127 + (info_button and 0 or 45) + (fullscreen_button and 0 or 45), y = refY - 30, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 500)
|
|
end
|
|
|
|
if user_opts.download_button then
|
|
lo = add_layout("download")
|
|
lo.geometry = {x = osc_geo.w - 172 + (ontop_button and 0 or 45) + (info_button and 0 or 45) + (fullscreen_button and 0 or 45), y = refY - 30, an = 5, w = 24, h = 24}
|
|
lo.style = osc_styles.control_3
|
|
lo.visible = (osc_param.playresx >= 400)
|
|
end
|
|
end
|
|
|
|
local function adjust_subtitles(visible)
|
|
if not mp.get_property_native("sid") then return end
|
|
|
|
local scale = state.fullscreen and user_opts.scalefullscreen or user_opts.scalewindowed
|
|
|
|
if visible and user_opts.raise_subtitles and state.osc_visible == true then
|
|
local w, h = mp.get_osd_size()
|
|
if h > 0 then
|
|
local raise_factor = user_opts.raise_subtitle_amount
|
|
|
|
-- adjust for scale
|
|
if scale > 1 then
|
|
raise_factor = raise_factor * (1 + (scale - 1) * 0.2)
|
|
elseif scale < 1 then
|
|
raise_factor = raise_factor * (0.8 + (scale - 0.5) * 0.5)
|
|
end
|
|
|
|
-- raise percentage
|
|
local raise_percent = (raise_factor / osc_param.playresy) * 100
|
|
|
|
-- don't adjust if user's sub-pos is higher than the raise factor
|
|
if state.user_subpos >= (100 - raise_percent) then
|
|
local adjusted = math.floor((osc_param.playresy - raise_factor) / osc_param.playresy * 100)
|
|
if adjusted < 0 then adjusted = state.user_subpos end
|
|
|
|
state.osc_adjusted_subpos = adjusted
|
|
mp.set_property_number("sub-pos", adjusted)
|
|
else
|
|
state.osc_adjusted_subpos = nil
|
|
end
|
|
end
|
|
elseif user_opts.raise_subtitles then
|
|
-- restore user's original subtitle position
|
|
if state.user_subpos then
|
|
mp.set_property_number("sub-pos", state.user_subpos)
|
|
end
|
|
state.osc_adjusted_subpos = nil
|
|
end
|
|
end
|
|
|
|
local function is_image()
|
|
local current_track = mp.get_property_native("current-tracks/video")
|
|
if current_track and current_track.image and not current_track.albumart then
|
|
state.is_image = true
|
|
else
|
|
state.is_image = false
|
|
end
|
|
end
|
|
|
|
local function osc_visible(visible)
|
|
if state.osc_visible ~= visible then
|
|
state.osc_visible = visible
|
|
update_margins()
|
|
adjust_subtitles(true)
|
|
end
|
|
request_tick()
|
|
end
|
|
|
|
local function command_callback(command)
|
|
if command ~= "" and command ~= "ignore" then
|
|
return function ()
|
|
mp.command(command)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function osc_init()
|
|
msg.debug("osc_init")
|
|
|
|
-- set canvas resolution according to display aspect and scaling setting
|
|
local baseResY = 720
|
|
local _, display_h, display_aspect = mp.get_osd_size()
|
|
local scale
|
|
|
|
if state.fullscreen then
|
|
scale = user_opts.scalefullscreen
|
|
else
|
|
scale = user_opts.scalewindowed
|
|
end
|
|
|
|
local scale_with_video
|
|
if user_opts.vidscale == "auto" then
|
|
scale_with_video = mp.get_property_native("osd-scale-by-window")
|
|
else
|
|
scale_with_video = user_opts.vidscale == "yes"
|
|
end
|
|
|
|
if scale_with_video then
|
|
osc_param.unscaled_y = baseResY
|
|
else
|
|
osc_param.unscaled_y = display_h
|
|
end
|
|
osc_param.playresy = osc_param.unscaled_y / scale
|
|
if display_aspect > 0 then
|
|
osc_param.display_aspect = display_aspect
|
|
end
|
|
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
|
|
|
-- stop seeking with the slider to prevent skipping files
|
|
state.active_element = nil
|
|
|
|
elements = {}
|
|
|
|
-- some often needed stuff
|
|
local pl_count = mp.get_property_number("playlist-count", 0)
|
|
local have_pl = pl_count > 1
|
|
local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
|
|
local have_ch = mp.get_property_number("chapters", 0) > 0
|
|
local loop = mp.get_property("loop-playlist", "no")
|
|
|
|
local nojumpoffset = user_opts.jump_buttons and 0 or 100
|
|
local noskipoffset = user_opts.chapter_skip_buttons and 0 or 100
|
|
local outeroffset = (user_opts.chapter_skip_buttons and 0 or 100) + (user_opts.jump_buttons and 0 or 100)
|
|
local audio_offset = (audio_track_count == 0 or not mp.get_property_native("aid")) and 100 or 0
|
|
local sub_offset = (sub_track_count == 0 or not mp.get_property_native("sid")) and 100 or 0
|
|
local playlist_offset = not have_pl and 100 or 0
|
|
|
|
local ne
|
|
|
|
-- Window controls
|
|
-- Close: 🗙
|
|
ne = new_element("close", "button")
|
|
ne.content = icons.window.close
|
|
ne.eventresponder["mbtn_left_up"] = function () mp.commandv("quit") end
|
|
|
|
-- Minimize: 🗕
|
|
ne = new_element("minimize", "button")
|
|
ne.content = icons.window.minimize
|
|
ne.eventresponder["mbtn_left_up"] = function () mp.commandv("cycle", "window-minimized") end
|
|
|
|
-- Maximize: 🗖 /🗗
|
|
ne = new_element("maximize", "button")
|
|
ne.content = (state.maximized or state.fullscreen) and icons.window.unmaximize or icons.window.maximize
|
|
ne.eventresponder["mbtn_left_up"] = function () mp.commandv("cycle", (state.fullscreen and "fullscreen" or "window-maximized")) end
|
|
|
|
-- Window Title
|
|
ne = new_element("windowtitle", "button")
|
|
ne.content = function ()
|
|
local title = mp.command_native({"expand-text", user_opts.windowcontrols_title}) or ""
|
|
title = title:gsub("\n", " ")
|
|
return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv"
|
|
end
|
|
|
|
-- OSC title
|
|
ne = new_element("title", "button")
|
|
ne.visible = user_opts.show_title
|
|
ne.content = function ()
|
|
local title = state.forced_title or mp.command_native({"expand-text", user_opts.title})
|
|
title = title:gsub("\n", " ")
|
|
return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv"
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.title_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.title_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.title_mbtn_mid_command)
|
|
|
|
-- Chapter title (above seekbar)
|
|
ne = new_element("chapter_title", "button")
|
|
ne.visible = mp.get_property_number("chapter", -1) >= 0
|
|
ne.content = function()
|
|
local chapter_index = mp.get_property_number("chapter", -1)
|
|
if user_opts.chapter_fmt == "no" or chapter_index < 0 then
|
|
return ""
|
|
end
|
|
|
|
local chapters = mp.get_property_native("chapter-list", {})
|
|
local chapter_data = chapters[chapter_index + 1]
|
|
local chapter_title = chapter_data and chapter_data.title ~= "" and chapter_data.title
|
|
or string.format("%s: %d/%d", locale.chapter, chapter_index + 1, #chapters)
|
|
|
|
chapter_title = mp.command_native({"escape-ass", chapter_title})
|
|
if thumbfast.disabled and not user_opts.show_title and state.forced_title then
|
|
chapter_title = state.forced_title
|
|
end
|
|
|
|
return string.format(user_opts.chapter_fmt, chapter_title)
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.chapter_title_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.chapter_title_mbtn_right_command)
|
|
|
|
-- playlist buttons
|
|
-- prev
|
|
ne = new_element("playlist_prev", "button")
|
|
ne.visible = (osc_param.playresx >= (state.is_image and 300 or 500) - nojumpoffset - noskipoffset*(nojumpoffset == 0 and 1 or 10))
|
|
ne.content = icons.previous
|
|
ne.enabled = (pl_pos > 1) or (loop ~= "no") or contains(user_opts.buttons_always_active, "playlist_prev")
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.playlist_prev_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.playlist_prev_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.playlist_prev_mbtn_mid_command)
|
|
|
|
--next
|
|
ne = new_element("playlist_next", "button")
|
|
ne.visible = (osc_param.playresx >= (state.is_image and 300 or 500) - nojumpoffset - noskipoffset*(nojumpoffset == 0 and 1 or 10))
|
|
ne.content = icons.next
|
|
ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no") or contains(user_opts.buttons_always_active, "playlist_next")
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.playlist_next_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.playlist_next_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.playlist_next_mbtn_mid_command)
|
|
|
|
--play control buttons
|
|
--play_pause
|
|
ne = new_element("play_pause", "button")
|
|
ne.content = function ()
|
|
if mp.get_property("eof-reached") == "yes" then
|
|
return icons.replay
|
|
elseif mp.get_property("pause") == "yes" and not state.playing_and_seeking then
|
|
return icons.play
|
|
else
|
|
return icons.pause
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
if mp.get_property("eof-reached") == "yes" then
|
|
mp.commandv("seek", 0, "absolute-percent")
|
|
mp.commandv("set", "pause", "no")
|
|
else
|
|
mp.commandv("cycle", "pause")
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_right_down"] = function ()
|
|
if user_opts.loop_in_pause then
|
|
mp.command("show-text '" .. (state.looping and locale.loop_disable or locale.loop_enable) .. "'")
|
|
state.looping = not state.looping
|
|
mp.set_property_native("loop-file", state.looping)
|
|
end
|
|
end
|
|
|
|
local jump_amount = user_opts.jump_amount
|
|
local jump_more_amount = user_opts.jump_more_amount
|
|
local jump_mode = user_opts.jump_mode
|
|
local jump_icon = user_opts.jump_icon_number and icons.jump[jump_amount] or icons.jump.default
|
|
|
|
--jump_backward
|
|
ne = new_element("jump_backward", "button")
|
|
ne.softrepeat = user_opts.jump_softrepeat == true
|
|
ne.content = jump_icon[1]
|
|
ne.eventresponder["mbtn_left_down"] = function () mp.commandv("seek", -jump_amount, jump_mode) end
|
|
ne.eventresponder["mbtn_right_down"] = function () mp.commandv("seek", -jump_more_amount, jump_mode) end
|
|
ne.eventresponder["shift+mbtn_left_down"] = function () mp.commandv("frame-back-step") end
|
|
|
|
--jump_forward
|
|
ne = new_element("jump_forward", "button")
|
|
ne.softrepeat = user_opts.jump_softrepeat == true
|
|
ne.content = jump_icon[2]
|
|
ne.eventresponder["mbtn_left_down"] = function () mp.commandv("seek", jump_amount, jump_mode) end
|
|
ne.eventresponder["mbtn_right_down"] = function () mp.commandv("seek", jump_more_amount, jump_mode) end
|
|
ne.eventresponder["shift+mbtn_left_down"] = function () mp.commandv("frame-step") end
|
|
|
|
--chapter_backward
|
|
ne = new_element("chapter_backward", "button")
|
|
ne.visible = (osc_param.playresx >= 400 - nojumpoffset*10)
|
|
ne.softrepeat = user_opts.chapter_softrepeat == true
|
|
ne.content = icons.rewind
|
|
ne.enabled = (have_ch) -- disables button when no chapters available.
|
|
ne.eventresponder["mbtn_left_down"] = command_callback(user_opts.chapter_prev_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_down"] = command_callback(user_opts.chapter_prev_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.chapter_prev_mbtn_mid_command)
|
|
ne.eventresponder["shift+mbtn_right_down"] = function () mp.commandv("seek", -jump_more_amount, jump_mode) end
|
|
|
|
--chapter_forward
|
|
ne = new_element("chapter_forward", "button")
|
|
ne.visible = (osc_param.playresx >= 400 - nojumpoffset*10)
|
|
ne.softrepeat = user_opts.chapter_softrepeat == true
|
|
ne.content = icons.forward
|
|
ne.enabled = (have_ch) -- disables button when no chapters available.
|
|
ne.eventresponder["mbtn_left_down"] = command_callback(user_opts.chapter_next_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_down"] = command_callback(user_opts.chapter_next_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.chapter_next_mbtn_mid_command)
|
|
ne.eventresponder["shift+mbtn_right_down"] = function () mp.commandv("seek", jump_more_amount, jump_mode) end
|
|
|
|
update_tracklist()
|
|
|
|
local visible_min_width = 550 - outeroffset
|
|
|
|
--tog_playlist
|
|
ne = new_element("tog_playlist", "button")
|
|
ne.enabled = have_pl or not user_opts.gray_empty_playlist_button
|
|
ne.off = not have_pl and user_opts.gray_empty_playlist_button
|
|
ne.visible = (osc_param.playresx >= (state.is_image and 250 or visible_min_width) - outeroffset)
|
|
ne.content = icons.playlist
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.menu or ""
|
|
ne.nothingavailable = locale.no_playlist
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.playlist_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.playlist_mbtn_right_command)
|
|
visible_min_width = visible_min_width + (ne.enabled and 100 or 0)
|
|
|
|
--audio_track
|
|
ne = new_element("audio_track", "button")
|
|
ne.enabled = audio_track_count > 0
|
|
ne.off = audio_track_count == 0 or not mp.get_property_native("aid")
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.content = icons.audio
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function ()
|
|
local prop = mp.get_property("current-tracks/audio/title") or mp.get_property("current-tracks/audio/lang") or locale.na
|
|
return (locale.audio .. " " .. mp.get_property_number("aid", "-") .. "/" .. audio_track_count .. " [" .. prop .. "]")
|
|
end
|
|
ne.nothingavailable = locale.no_audio
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.audio_track_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.audio_track_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.audio_track_mbtn_mid_command)
|
|
ne.eventresponder["wheel_down_press"] = command_callback(user_opts.audio_track_wheel_down_command)
|
|
ne.eventresponder["wheel_up_press"] = command_callback(user_opts.audio_track_wheel_up_command)
|
|
visible_min_width = visible_min_width + (ne.enabled and 100 or 0)
|
|
|
|
--sub_track
|
|
ne = new_element("sub_track", "button")
|
|
ne.enabled = sub_track_count > 0
|
|
ne.off = sub_track_count == 0 or not mp.get_property_native("sid")
|
|
ne.visible = (osc_param.playresx >= visible_min_width - outeroffset)
|
|
ne.content = icons.subtitle
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function ()
|
|
local prop = mp.get_property("current-tracks/sub/title") or mp.get_property("current-tracks/sub/lang") or locale.na
|
|
return (locale.subtitle .. " " .. mp.get_property_number("sid", "-") .. "/" .. sub_track_count .. " [" .. prop .. "]")
|
|
end
|
|
ne.nothingavailable = locale.no_subs
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.sub_track_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.sub_track_mbtn_right_command)
|
|
ne.eventresponder["shift+mbtn_left_down"] = command_callback(user_opts.sub_track_mbtn_mid_command)
|
|
ne.eventresponder["wheel_down_press"] = command_callback(user_opts.sub_track_wheel_down_command)
|
|
ne.eventresponder["wheel_up_press"] = command_callback(user_opts.sub_track_wheel_up_command)
|
|
visible_min_width = visible_min_width + (ne.enabled and 100 or 0)
|
|
|
|
-- vol_ctrl
|
|
local vol_visible_offset = sub_offset + playlist_offset
|
|
ne = new_element("vol_ctrl", "button")
|
|
ne.enabled = audio_track_count > 0
|
|
ne.off = audio_track_count == 0
|
|
ne.visible = (osc_param.playresx >= 900 - vol_visible_offset - outeroffset) and user_opts.volume_control
|
|
ne.content = function ()
|
|
local volume = mp.get_property_number("volume", 0)
|
|
if state.mute then
|
|
return icons.volume_mute
|
|
else
|
|
if volume >= 75 then
|
|
return icons.volume_high
|
|
elseif volume >= 25 then
|
|
return icons.volume_low
|
|
else
|
|
return icons.volume_quiet
|
|
end
|
|
end
|
|
end
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function ()
|
|
local volume = mp.get_property_number("volume", 0) or 0
|
|
-- show only one decimal, if decimals exist
|
|
volume = volume % 1 == 0 and string.format("%.0f", volume) or string.format("%.1f", volume)
|
|
return volume
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.vol_ctrl_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.vol_ctrl_mbtn_right_command)
|
|
ne.eventresponder["wheel_up_press"] = command_callback(user_opts.vol_ctrl_wheel_up_command)
|
|
ne.eventresponder["wheel_down_press"] = command_callback(user_opts.vol_ctrl_wheel_down_command)
|
|
|
|
--volumebar
|
|
local volume_max = mp.get_property_number("volume-max") > 0 and mp.get_property_number("volume-max") or 100
|
|
ne = new_element("volumebar", "slider")
|
|
ne.visible = (osc_param.playresx >= 1150 - outeroffset) and user_opts.volume_control
|
|
ne.enabled = audio_track_count > 0
|
|
ne.slider = {min = {value = 0}, max = {value = volume_max}}
|
|
ne.slider.markerF = function () return {} end
|
|
ne.slider.seekRangesF = function() return nil end
|
|
ne.slider.posF = function ()
|
|
local volume = mp.get_property_number("volume")
|
|
if user_opts.volume_control == "logarithmic" then
|
|
return math.sqrt(volume * 100)
|
|
else
|
|
return volume
|
|
end
|
|
end
|
|
ne.slider.tooltipF = function (pos) return (audio_track_count > 0) and set_volume(pos) or "" end
|
|
ne.eventresponder["mouse_move"] = function (element)
|
|
local pos = get_slider_value(element)
|
|
local setvol = set_volume(pos)
|
|
if element.state.lastseek == nil or element.state.lastseek ~= setvol then
|
|
mp.commandv("osd-msg", "set", "volume", setvol)
|
|
element.state.lastseek = setvol
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_left_down"] = function (element)
|
|
local pos = get_slider_value(element)
|
|
mp.commandv("osd-msg", "set", "volume", set_volume(pos))
|
|
end
|
|
ne.eventresponder["reset"] = function (element) element.state.lastseek = nil end
|
|
ne.eventresponder["wheel_up_press"] = command_callback(user_opts.vol_ctrl_wheel_up_command)
|
|
ne.eventresponder["wheel_down_press"] = command_callback(user_opts.vol_ctrl_wheel_down_command)
|
|
|
|
-- zoom control
|
|
-- zoom out icon
|
|
local current_zoom = mp.get_property_number("video-zoom")
|
|
ne = new_element("zoom_out_icon", "button")
|
|
ne.visible = (osc_param.playresx >= 400)
|
|
ne.content = icons.zoom_out
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.zoom_out or ""
|
|
ne.eventresponder["mbtn_left_up"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.max(user_opts.zoom_out_min, current_zoom - 0.05)) end
|
|
ne.eventresponder["mbtn_right_up"] = function () mp.commandv("osd-msg", "set", "video-zoom", 0) end
|
|
ne.eventresponder["wheel_up_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.min(user_opts.zoom_in_max, current_zoom + 0.05)) end
|
|
ne.eventresponder["wheel_down_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.max(user_opts.zoom_out_min, current_zoom - 0.05)) end
|
|
|
|
-- zoom slider
|
|
ne = new_element("zoom_control", "slider")
|
|
ne.slider = {min = {value = user_opts.zoom_out_min}, max = {value = user_opts.zoom_in_max}}
|
|
ne.visible = (osc_param.playresx >= 400) and user_opts.zoom_control and state.is_image
|
|
ne.slider.markerF = function () return {} end
|
|
ne.slider.seekRangesF = function() return nil end
|
|
ne.slider.posF = function () return mp.get_property_number("video-zoom") end
|
|
ne.slider.tooltipF = function (pos) return string.format("%.3f", pos):gsub("%.?0*$", "") end
|
|
ne.eventresponder["mouse_move"] = function (element)
|
|
local pos = get_slider_value(element)
|
|
if element.state.lastseek == nil or element.state.lastseek ~= pos then
|
|
mp.commandv("osd-msg", "set", "video-zoom", pos)
|
|
element.state.lastseek = pos
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_left_down"] = function (element) mp.commandv("osd-msg", "set", "video-zoom", get_slider_value(element)) end
|
|
ne.eventresponder["reset"] = function (element) element.state.lastseek = nil end
|
|
ne.eventresponder["mbtn_right_up"] = function () mp.commandv("osd-msg", "set", "video-zoom", 0) end
|
|
ne.eventresponder["wheel_up_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.min(user_opts.zoom_in_max, current_zoom + 0.05)) end
|
|
ne.eventresponder["wheel_down_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.max(user_opts.zoom_out_min, current_zoom - 0.05)) end
|
|
|
|
-- zoom in icon
|
|
ne = new_element("zoom_in_icon", "button")
|
|
ne.visible = (osc_param.playresx >= 400)
|
|
ne.content = icons.zoom_in
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.zoom_in or ""
|
|
ne.eventresponder["mbtn_left_up"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.min(user_opts.zoom_in_max, current_zoom + 0.05)) end
|
|
ne.eventresponder["mbtn_right_up"] = function () mp.commandv("osd-msg", "set", "video-zoom", 0) end
|
|
ne.eventresponder["wheel_up_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.min(user_opts.zoom_in_max, current_zoom + 0.05)) end
|
|
ne.eventresponder["wheel_down_press"] = function () mp.commandv("osd-msg", "set", "video-zoom", math.max(user_opts.zoom_out_min, current_zoom - 0.05)) end
|
|
|
|
visible_min_width = 550 - outeroffset
|
|
--tog_fullscreen
|
|
ne = new_element("tog_fullscreen", "button")
|
|
ne.content = function () return state.fullscreen and icons.fullscreen_exit or icons.fullscreen end
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.fullscreen_mbtn_left_command)
|
|
ne.eventresponder["mbtn_right_up"] = command_callback(user_opts.fullscreen_mbtn_right_command)
|
|
visible_min_width = visible_min_width + (user_opts.fullscreen_button and 100 or 0)
|
|
|
|
--tog_info
|
|
ne = new_element("tog_info", "button")
|
|
ne.content = icons.info
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.stats_info or ""
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.eventresponder["mbtn_left_up"] = command_callback(user_opts.info_mbtn_left_command)
|
|
visible_min_width = visible_min_width + (user_opts.info_button and 100 or 0)
|
|
|
|
--tog_ontop
|
|
ne = new_element("tog_ontop", "button")
|
|
ne.content = function () return mp.get_property("ontop") == "no" and icons.ontop_on or icons.ontop_off end
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function () return user_opts.tooltip_hints and (mp.get_property("ontop") == "no" and locale.ontop or locale.ontop_disable) or "" end
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
mp.commandv("cycle", "ontop")
|
|
if state.initialborder == "yes" then
|
|
if mp.get_property("ontop") == "yes" then
|
|
mp.commandv("set", "border", "no")
|
|
else
|
|
mp.commandv("set", "border", "yes")
|
|
end
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_right_up"] = function ()
|
|
mp.commandv("cycle", "ontop")
|
|
if mp.get_property("border") == "no" then
|
|
mp.commandv("set", "border", "yes")
|
|
end
|
|
end
|
|
visible_min_width = visible_min_width + (user_opts.ontop_button and 100 or 0)
|
|
|
|
--screenshot
|
|
ne = new_element("screenshot", "button")
|
|
ne.content = icons.screenshot
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.screenshot or ""
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
local temp_sub_pos = mp.get_property("sub-pos")
|
|
if user_opts.screenshot_flag == "subtitles" or user_opts.screenshot_flag == "subtitles+each-frame" then
|
|
mp.commandv("set", "sub-pos", 100)
|
|
end
|
|
mp.commandv("osd-msg", "screenshot", user_opts.screenshot_flag)
|
|
mp.commandv("set", "sub-pos", temp_sub_pos)
|
|
end
|
|
visible_min_width = visible_min_width + (user_opts.screenshot_button and 100 or 0)
|
|
|
|
--tog_loop
|
|
ne = new_element("tog_loop", "button")
|
|
ne.content = function() return state.looping and icons.loop_on or icons.loop_off end
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function() return user_opts.tooltip_hints and (state.looping and locale.loop_disable or locale.loop_enable) or "" end
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
mp.command("show-text '" .. (state.looping and locale.loop_disable or locale.loop_enable) .. "'")
|
|
state.looping = not state.looping
|
|
mp.set_property_native("loop-file", state.looping)
|
|
end
|
|
visible_min_width = visible_min_width + (user_opts.loop_button and 100 or 0)
|
|
|
|
--tog_speed
|
|
ne = new_element("tog_speed", "button")
|
|
ne.content = icons.speed
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = user_opts.tooltip_hints and locale.speed_control or ""
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
mp.commandv("osd-msg", "set", "speed", math.min(100, mp.get_property_number("speed") + user_opts.speed_button_click))
|
|
end
|
|
ne.eventresponder["mbtn_right_up"] = function () mp.commandv("osd-msg", "set", "speed", 1) end
|
|
ne.eventresponder["wheel_up_press"] = function ()
|
|
mp.commandv("osd-msg", "set", "speed", math.min(100, mp.get_property_number("speed") + user_opts.speed_button_scroll))
|
|
end
|
|
ne.eventresponder["wheel_down_press"] = function ()
|
|
mp.commandv("osd-msg", "set", "speed", math.max(0.25, mp.get_property_number("speed") - user_opts.speed_button_scroll))
|
|
end
|
|
visible_min_width = visible_min_width + (user_opts.speed_button and 100 or 0)
|
|
|
|
--download
|
|
ne = new_element("download", "button")
|
|
ne.content = function () return state.downloading and icons.downloading or icons.download end
|
|
ne.visible = (osc_param.playresx >= visible_min_width) and state.is_URL
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = function () return state.downloading and locale.downloading .. "..." or locale.download .. " (" .. state.file_size_normalized .. ")" end
|
|
ne.eventresponder["mbtn_left_up"] = function ()
|
|
local localpath = mp.command_native({"expand-path", user_opts.download_path})
|
|
|
|
if state.downloaded_once then
|
|
mp.commandv("show-text", locale.downloaded)
|
|
elseif state.downloading then
|
|
mp.commandv("show-text", locale.download_in_progress)
|
|
else
|
|
mp.commandv("show-text", locale.downloading .. "...")
|
|
state.downloading = true
|
|
-- use current or default ytdl-format
|
|
local mpv_ytdl = mp.get_property("file-local-options/ytdl-format") or mp.get_property("ytdl-format") or ""
|
|
local ytdl_format = (mpv_ytdl and mpv_ytdl ~= "") and "-f " .. mpv_ytdl or "-f " .. "bestvideo+bestaudio/best"
|
|
local command = {
|
|
"yt-dlp",
|
|
state.is_image and "" or ytdl_format,
|
|
state.is_image and "" or "--remux", state.is_image and "" or "mp4",
|
|
"--add-metadata",
|
|
"--embed-subs",
|
|
"-o", "%(title)s.%(ext)s",
|
|
"-P", localpath,
|
|
state.url_path
|
|
}
|
|
|
|
local status = exec(command, download_done)
|
|
end
|
|
end
|
|
visible_min_width = visible_min_width + (user_opts.download_button and 100 or 0)
|
|
|
|
|
|
-- cache info
|
|
ne = new_element("cache_info", "button")
|
|
ne.visible = (osc_param.playresx >= visible_min_width)
|
|
ne.content = function ()
|
|
if not cache_enabled() then return "" end
|
|
local dmx_cache = state.cache_state["cache-duration"]
|
|
local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s
|
|
if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then
|
|
state.dmx_cache = dmx_cache
|
|
else
|
|
dmx_cache = state.dmx_cache
|
|
end
|
|
local min = math.floor(dmx_cache / 60)
|
|
local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60
|
|
local cache_time = (min > 0 and string.format("%sm%02.0fs", min, sec) or string.format("%3.0fs", sec))
|
|
|
|
local dmx_speed = state.cache_state["raw-input-rate"] or 0
|
|
local cache_speed = utils.format_bytes_humanized(dmx_speed)
|
|
local number, unit = cache_speed:match("([%d%.]+)%s*(%S+)")
|
|
local cache_info = state.buffering and locale.buffering .. ": " .. mp.get_property("cache-buffering-state") .. "%" or cache_time
|
|
local cache_info_speed = string.format("%8s %4s/s", number, unit)
|
|
|
|
return user_opts.cache_info_speed and cache_info .. "\\N" .. cache_info_speed or cache_info
|
|
end
|
|
ne.tooltip_style = osc_styles.tooltip
|
|
ne.tooltipF = (user_opts.tooltip_hints and cache_enabled()) and locale.cache or ""
|
|
ne.eventresponder["mbtn_left_up"] = function() mp.command("script-binding stats/display-page-3") end
|
|
|
|
--seekbar
|
|
ne = new_element("seekbar", "slider")
|
|
ne.enabled = mp.get_property("percent-pos") ~= nil
|
|
ne.thumbnailable = true
|
|
state.slider_element = ne.enabled and ne or nil -- used for forced_title
|
|
ne.slider.markerF = function ()
|
|
local duration = mp.get_property_number("duration")
|
|
if duration ~= nil then
|
|
local chapters = mp.get_property_native("chapter-list", {})
|
|
local markers = {}
|
|
for n = 1, #chapters do
|
|
markers[n] = (chapters[n].time / duration * 100)
|
|
end
|
|
return markers
|
|
else
|
|
return {}
|
|
end
|
|
end
|
|
ne.slider.posF = function ()
|
|
if mp.get_property_bool("eof-reached") then return 100 end
|
|
return mp.get_property_number("percent-pos")
|
|
end
|
|
ne.slider.tooltipF = function (pos)
|
|
state.touchingprogressbar = true
|
|
local duration = mp.get_property_number("duration")
|
|
if duration ~= nil and pos ~= nil then
|
|
local possec = duration * (pos / 100)
|
|
local time = mp.format_time(possec)
|
|
-- If video is less than 1 hour, and the time format is not fixed, strip the "00:" prefix
|
|
if possec < 3600 and user_opts.time_format ~= "fixed" then
|
|
time = time:gsub("^00:", "")
|
|
end
|
|
return time
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
ne.slider.seekRangesF = function()
|
|
if not user_opts.seekrange or not cache_enabled() then
|
|
return nil
|
|
end
|
|
local duration = mp.get_property_number("duration")
|
|
if duration == nil or duration <= 0 then
|
|
return nil
|
|
end
|
|
local nranges = {}
|
|
for _, range in pairs(state.cache_state["seekable-ranges"]) do
|
|
nranges[#nranges + 1] = {
|
|
["start"] = 100 * range["start"] / duration,
|
|
["end"] = 100 * range["end"] / duration,
|
|
}
|
|
end
|
|
return nranges
|
|
end
|
|
ne.eventresponder["mouse_move"] = function (element)
|
|
if not element.state.mbtnleft then return end -- allow drag for mbtnleft only!
|
|
-- mouse move events may pile up during seeking and may still get
|
|
-- sent when the user is done seeking, so we need to throw away
|
|
-- identical seeks
|
|
state.playing_and_seeking = true
|
|
if mp.get_property("pause") == "no" and user_opts.mouse_seek_pause then
|
|
mp.commandv("cycle", "pause")
|
|
end
|
|
local seekto = get_slider_value(element)
|
|
if element.state.lastseek == nil or
|
|
element.state.lastseek ~= seekto then
|
|
local flags = "absolute-percent"
|
|
if not user_opts.seekbarkeyframes then
|
|
flags = flags .. "+exact"
|
|
end
|
|
mp.commandv("seek", seekto, flags)
|
|
element.state.lastseek = seekto
|
|
end
|
|
end
|
|
ne.eventresponder["mbtn_left_down"] = function (element)
|
|
element.state.mbtnleft = true
|
|
mp.commandv("seek", get_slider_value(element), "absolute-percent+exact")
|
|
end
|
|
ne.eventresponder["shift+mbtn_left_down"] = function (element)
|
|
element.state.mbtnleft = true
|
|
mp.commandv("seek", get_slider_value(element), "absolute-percent")
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = function (element)
|
|
element.state.mbtnleft = false
|
|
end
|
|
ne.eventresponder["mbtn_right_down"] = function (element)
|
|
local chapter
|
|
local pos = get_slider_value(element)
|
|
local diff = math.huge
|
|
|
|
for i, marker in ipairs(element.slider.markerF()) do
|
|
if math.abs(pos - marker) < diff then
|
|
diff = math.abs(pos - marker)
|
|
chapter = i
|
|
end
|
|
end
|
|
|
|
if chapter then
|
|
mp.set_property("chapter", chapter - 1)
|
|
end
|
|
end
|
|
ne.eventresponder["reset"] = function (element)
|
|
element.state.lastseek = nil
|
|
if state.playing_and_seeking then
|
|
if mp.get_property("eof-reached") == "no" and user_opts.mouse_seek_pause then
|
|
mp.commandv("cycle", "pause")
|
|
end
|
|
state.playing_and_seeking = false
|
|
end
|
|
end
|
|
ne.eventresponder["wheel_up_press"] = function () mp.commandv("seek", 10) end
|
|
ne.eventresponder["wheel_down_press"] = function () mp.commandv("seek", -10) end
|
|
|
|
--persistent seekbar
|
|
ne = new_element("persistentseekbar", "slider")
|
|
ne.enabled = mp.get_property("percent-pos") ~= nil
|
|
state.slider_element = ne.enabled and ne or nil -- used for forced_title
|
|
ne.slider.markerF = function () return {} end
|
|
ne.slider.posF = function ()
|
|
if mp.get_property_bool("eof-reached") then return 100 end
|
|
return mp.get_property_number("percent-pos")
|
|
end
|
|
ne.slider.tooltipF = function() return "" end
|
|
ne.slider.seekRangesF = function()
|
|
if user_opts.persistentbuffer then
|
|
if not user_opts.seekrange then
|
|
return nil
|
|
end
|
|
local cache_state = state.cache_state
|
|
if not cache_state then
|
|
return nil
|
|
end
|
|
local duration = mp.get_property_number("duration")
|
|
if duration == nil or duration <= 0 then
|
|
return nil
|
|
end
|
|
local ranges = cache_state["seekable-ranges"]
|
|
if #ranges == 0 then
|
|
return nil
|
|
end
|
|
local nranges = {}
|
|
for _, range in pairs(ranges) do
|
|
nranges[#nranges + 1] = {
|
|
["start"] = 100 * range["start"] / duration,
|
|
["end"] = 100 * range["end"] / duration,
|
|
}
|
|
end
|
|
return nranges
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Helper function to format time
|
|
local function format_time(seconds)
|
|
if not seconds then return "--:--" end
|
|
|
|
local hours = math.floor(seconds / 3600)
|
|
local minutes = math.floor((seconds % 3600) / 60)
|
|
local whole_seconds = math.floor(seconds % 60)
|
|
local milliseconds = state.tc_ms and math.floor((seconds % 1) * 1000) or nil
|
|
|
|
-- Always show HH:MM:SS if user_opts.time_format is "fixed"
|
|
local force_hours = user_opts.time_format == "fixed"
|
|
|
|
-- Format string templates
|
|
local format_with_ms = (hours > 0 or force_hours) and "%02d:%02d:%02d.%03d" or "%02d:%02d.%03d"
|
|
local format_without_ms = (hours > 0 or force_hours) and "%02d:%02d:%02d" or "%02d:%02d"
|
|
|
|
if state.tc_ms then
|
|
return string.format(format_with_ms,
|
|
(hours > 0 or force_hours) and hours or minutes,
|
|
(hours > 0 or force_hours) and minutes or whole_seconds,
|
|
(hours > 0 or force_hours) and whole_seconds or milliseconds,
|
|
(hours > 0 or force_hours) and milliseconds or nil)
|
|
else
|
|
return string.format(format_without_ms,
|
|
(hours > 0 or force_hours) and hours or minutes,
|
|
(hours > 0 or force_hours) and minutes or whole_seconds,
|
|
(hours > 0 or force_hours) and whole_seconds or nil)
|
|
end
|
|
end
|
|
|
|
-- Time codes display
|
|
local tc_visible_offset = audio_offset + sub_offset + playlist_offset
|
|
ne = new_element("time_codes", "button")
|
|
ne.visible = mp.get_property_number("duration", 0) > 0
|
|
ne.content = function()
|
|
local playback_time = mp.get_property_number("playback-time", 0)
|
|
|
|
-- call request_init() only when needed to update time code width
|
|
if user_opts.time_format ~= "fixed" and playback_time then
|
|
local hour_or_more = playback_time >= 3600
|
|
if hour_or_more ~= state.playtime_hour_force_init then
|
|
request_init()
|
|
state.playtime_hour_force_init = hour_or_more
|
|
state.playtime_nohour_force_init = not hour_or_more
|
|
end
|
|
end
|
|
|
|
local duration = mp.get_property_number("duration", 0)
|
|
if duration <= 0 then return "--:--" end
|
|
|
|
local playtime_remaining = state.tc_right_rem and
|
|
mp.get_property_number("playtime-remaining", 0) or duration
|
|
|
|
local prefix = state.tc_right_rem and
|
|
(user_opts.unicodeminus and UNICODE_MINUS or "-") or ""
|
|
|
|
return format_time(playback_time) .. " / " .. prefix .. format_time(playtime_remaining)
|
|
end
|
|
ne.eventresponder["mbtn_left_up"] = function()
|
|
state.tc_right_rem = not state.tc_right_rem
|
|
end
|
|
ne.eventresponder["mbtn_right_up"] = function()
|
|
state.tc_ms = not state.tc_ms
|
|
request_init()
|
|
end
|
|
|
|
-- load layout
|
|
if state.is_image then
|
|
layouts["modern-image"]()
|
|
else
|
|
layouts["modern"]()
|
|
end
|
|
|
|
-- load window controls
|
|
if window_controls_enabled() then
|
|
window_controls()
|
|
end
|
|
|
|
--do something with the elements
|
|
prepare_elements()
|
|
update_margins()
|
|
end
|
|
|
|
local function show_osc()
|
|
-- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
|
|
if not state.enabled then return end
|
|
|
|
msg.trace("show_osc")
|
|
--remember last time of invocation (mouse move)
|
|
state.showtime = mp.get_time()
|
|
|
|
if user_opts.fadeduration <= 0 then
|
|
osc_visible(true)
|
|
elseif user_opts.fadein then
|
|
if not state.osc_visible then
|
|
state.anitype = "in"
|
|
request_tick()
|
|
end
|
|
else
|
|
osc_visible(true)
|
|
state.anitype = nil
|
|
end
|
|
end
|
|
|
|
local function hide_osc()
|
|
msg.trace("hide_osc")
|
|
if thumbfast.width ~= 0 and thumbfast.height ~= 0 then
|
|
mp.commandv("script-message-to", "thumbfast", "clear")
|
|
end
|
|
if not state.enabled then
|
|
-- typically hide happens at render() from tick(), but now tick() is
|
|
-- no-op and won't render again to remove the osc, so do that manually.
|
|
state.osc_visible = false
|
|
adjust_subtitles(false)
|
|
render_wipe()
|
|
elseif user_opts.fadeduration > 0 then
|
|
if state.osc_visible then
|
|
state.anitype = "out"
|
|
request_tick()
|
|
end
|
|
else
|
|
osc_visible(false)
|
|
end
|
|
end
|
|
|
|
local function pause_state(_, enabled)
|
|
state.paused = enabled
|
|
request_tick()
|
|
end
|
|
|
|
local function cache_state(_, st)
|
|
state.cache_state = st
|
|
request_tick()
|
|
end
|
|
|
|
local function mouse_leave()
|
|
state.touchtime = nil
|
|
|
|
if get_hidetimeout() >= 0 and get_touchtimeout() <= 0 then
|
|
local elapsed_time = mp.get_time() - state.showtime
|
|
|
|
if elapsed_time >= (get_hidetimeout() / 1000) then
|
|
hide_osc()
|
|
end
|
|
end
|
|
|
|
-- reset mouse position
|
|
state.last_mouseX, state.last_mouseY = nil, nil
|
|
state.mouse_in_window = false
|
|
end
|
|
|
|
local function handle_touch()
|
|
--remember last time of invocation (touch event)
|
|
state.touchtime = mp.get_time()
|
|
end
|
|
|
|
--
|
|
-- Event handling
|
|
--
|
|
local function reset_timeout()
|
|
state.showtime = mp.get_time()
|
|
end
|
|
|
|
local function element_has_action(element, action)
|
|
return element and element.eventresponder and
|
|
element.eventresponder[action]
|
|
end
|
|
|
|
local function process_event(source, what)
|
|
local action = string.format("%s%s", source,
|
|
what and ("_" .. what) or "")
|
|
|
|
if what == "down" or what == "press" then
|
|
reset_timeout() -- clicking resets the hideosc timer
|
|
|
|
for n = 1, #elements do
|
|
if mouse_hit(elements[n]) and
|
|
elements[n].eventresponder and
|
|
(elements[n].eventresponder[source .. "_up"] or
|
|
elements[n].eventresponder[action]) then
|
|
|
|
if what == "down" then
|
|
state.active_element = n
|
|
state.active_event_source = source
|
|
end
|
|
-- fire the down or press event if the element has one
|
|
if element_has_action(elements[n], action) then
|
|
elements[n].eventresponder[action](elements[n])
|
|
end
|
|
end
|
|
end
|
|
elseif what == "up" then
|
|
if elements[state.active_element] then
|
|
local n = state.active_element
|
|
|
|
if n == 0 then
|
|
--click on background (does not work)
|
|
elseif element_has_action(elements[n], action) and
|
|
mouse_hit(elements[n]) then
|
|
|
|
elements[n].eventresponder[action](elements[n])
|
|
end
|
|
|
|
--reset active element
|
|
if element_has_action(elements[n], "reset") then
|
|
elements[n].eventresponder["reset"](elements[n])
|
|
end
|
|
end
|
|
state.active_element = nil
|
|
state.mouse_down_counter = 0
|
|
elseif source == "mouse_move" then
|
|
state.mouse_in_window = true
|
|
|
|
local mouseX, mouseY = get_virt_mouse_pos()
|
|
if user_opts.minmousemove == 0 or
|
|
((state.last_mouseX ~= nil and state.last_mouseY ~= nil) and
|
|
((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
|
|
or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
|
|
)
|
|
) then
|
|
if user_opts.bottomhover then -- if enabled, only show osc if mouse is hovering at the bottom of the screen (where the UI elements are)
|
|
local top_hover = window_controls_enabled() and (user_opts.window_title or user_opts.window_controls)
|
|
if mouseY > osc_param.playresy - (user_opts.bottomhover_zone or 130)
|
|
or ((user_opts.window_top_bar == "yes" or not (state.border and state.title_bar)) or state.fullscreen) and (mouseY < 40 and top_hover) then
|
|
show_osc()
|
|
else
|
|
state.touchtime = nil
|
|
|
|
if get_hidetimeout() >= 0 and get_touchtimeout() <= 0 then
|
|
local elapsed_time = mp.get_time() - state.showtime
|
|
|
|
if elapsed_time >= (get_hidetimeout() / 1000) then
|
|
hide_osc()
|
|
end
|
|
end
|
|
end
|
|
else
|
|
show_osc()
|
|
end
|
|
end
|
|
state.last_mouseX, state.last_mouseY = mouseX, mouseY
|
|
|
|
local n = state.active_element
|
|
if element_has_action(elements[n], action) then
|
|
elements[n].eventresponder[action](elements[n])
|
|
end
|
|
end
|
|
|
|
-- ensure rendering after any (mouse) event - icons could change etc
|
|
request_tick()
|
|
end
|
|
|
|
local function do_enable_keybindings()
|
|
if state.enabled then
|
|
if not state.showhide_enabled then
|
|
mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
|
|
mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor")
|
|
end
|
|
state.showhide_enabled = true
|
|
end
|
|
end
|
|
|
|
local function enable_osc(enable)
|
|
state.enabled = enable
|
|
if enable then
|
|
do_enable_keybindings()
|
|
else
|
|
hide_osc() -- acts immediately when state.enabled == false
|
|
if state.showhide_enabled then
|
|
mp.disable_key_bindings("showhide")
|
|
mp.disable_key_bindings("showhide_wc")
|
|
end
|
|
state.showhide_enabled = false
|
|
end
|
|
end
|
|
|
|
local function render()
|
|
msg.trace("rendering")
|
|
local current_screen_sizeX, current_screen_sizeY = mp.get_osd_size()
|
|
local mouseX, mouseY = get_virt_mouse_pos()
|
|
local now = mp.get_time()
|
|
|
|
-- check if display changed, if so request reinit
|
|
if state.screen_sizeX ~= current_screen_sizeX
|
|
or state.screen_sizeY ~= current_screen_sizeY then
|
|
|
|
request_init_resize()
|
|
|
|
state.screen_sizeX = current_screen_sizeX
|
|
state.screen_sizeY = current_screen_sizeY
|
|
end
|
|
|
|
-- init management
|
|
if state.active_element then
|
|
-- mouse is held down on some element - keep ticking and ignore initReq
|
|
-- till it's released, or else the mouse-up (click) will misbehave or
|
|
-- get ignored. that's because osc_init() recreates the osc elements,
|
|
-- but mouse handling depends on the elements staying unmodified
|
|
-- between mouse-down and mouse-up (using the index active_element).
|
|
request_tick()
|
|
elseif state.initREQ then
|
|
osc_init()
|
|
state.initREQ = false
|
|
|
|
-- store initial mouse position
|
|
if (state.last_mouseX == nil or state.last_mouseY == nil)
|
|
and not (mouseX == nil or mouseY == nil) then
|
|
|
|
state.last_mouseX, state.last_mouseY = mouseX, mouseY
|
|
end
|
|
end
|
|
|
|
-- fade animation
|
|
if state.anitype ~= nil then
|
|
if state.anistart == nil then
|
|
state.anistart = now
|
|
end
|
|
|
|
if now < state.anistart + (user_opts.fadeduration / 1000) then
|
|
if state.anitype == "in" then --fade in
|
|
osc_visible(true)
|
|
state.animation = scale_value(state.anistart,
|
|
(state.anistart + (user_opts.fadeduration / 1000)),
|
|
255, 0, now)
|
|
elseif state.anitype == "out" then --fade out
|
|
state.animation = scale_value(state.anistart,
|
|
(state.anistart + (user_opts.fadeduration / 1000)),
|
|
0, 255, now)
|
|
end
|
|
else
|
|
if state.anitype == "out" then
|
|
osc_visible(false)
|
|
end
|
|
kill_animation()
|
|
end
|
|
else
|
|
kill_animation()
|
|
end
|
|
|
|
--mouse show/hide area
|
|
for _, cords in pairs(osc_param.areas["showhide"]) do
|
|
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
|
|
end
|
|
if osc_param.areas["showhide_wc"] then
|
|
for _, cords in pairs(osc_param.areas["showhide_wc"]) do
|
|
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc")
|
|
end
|
|
else
|
|
set_virt_mouse_area(0, 0, 0, 0, "showhide_wc")
|
|
end
|
|
do_enable_keybindings()
|
|
|
|
--mouse input area
|
|
local mouse_over_osc = false
|
|
|
|
for _,cords in ipairs(osc_param.areas["input"]) do
|
|
if state.osc_visible then -- activate only when OSC is actually visible
|
|
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
|
|
end
|
|
if state.osc_visible ~= state.input_enabled then
|
|
if state.osc_visible then
|
|
mp.enable_key_bindings("input")
|
|
else
|
|
mp.disable_key_bindings("input")
|
|
end
|
|
state.input_enabled = state.osc_visible
|
|
end
|
|
|
|
if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
|
|
mouse_over_osc = true
|
|
end
|
|
end
|
|
|
|
if osc_param.areas["window-controls"] then
|
|
for _,cords in ipairs(osc_param.areas["window-controls"]) do
|
|
if state.osc_visible then -- activate only when OSC is actually visible
|
|
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
|
|
mp.enable_key_bindings("window-controls")
|
|
else
|
|
mp.disable_key_bindings("window-controls")
|
|
end
|
|
|
|
if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
|
|
mouse_over_osc = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if osc_param.areas["window-controls-title"] then
|
|
for _,cords in ipairs(osc_param.areas["window-controls-title"]) do
|
|
if state.osc_visible then -- activate only when OSC is actually visible
|
|
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls-title")
|
|
end
|
|
if state.osc_visible ~= state.windowcontrols_title then
|
|
if state.osc_visible then
|
|
mp.enable_key_bindings("window-controls-title", "allow-vo-dragging")
|
|
else
|
|
mp.disable_key_bindings("window-controls-title", "allow-vo-dragging")
|
|
end
|
|
state.windowcontrols_title = state.osc_visible
|
|
end
|
|
|
|
if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
|
|
mouse_over_osc = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- autohide
|
|
if state.showtime ~= nil and get_hidetimeout() >= 0 then
|
|
local timeout = state.showtime + (get_hidetimeout() / 1000) - now
|
|
if timeout <= 0 and get_touchtimeout() <= 0 then
|
|
if state.active_element == nil and not mouse_over_osc then
|
|
hide_osc()
|
|
end
|
|
else
|
|
-- the timer is only used to recheck the state and to possibly run
|
|
-- the code above again
|
|
if not state.hide_timer then
|
|
state.hide_timer = mp.add_timeout(0, tick)
|
|
end
|
|
state.hide_timer.timeout = timeout
|
|
-- re-arm
|
|
state.hide_timer:kill()
|
|
state.hide_timer:resume()
|
|
end
|
|
end
|
|
|
|
-- actual rendering
|
|
local ass = assdraw.ass_new()
|
|
|
|
-- actual OSC
|
|
if state.osc_visible then
|
|
render_elements(ass)
|
|
end
|
|
|
|
if user_opts.persistentprogress or state.persistent_progress_toggle then
|
|
render_persistentprogressbar(ass)
|
|
end
|
|
|
|
-- submit
|
|
set_osd(osc_param.playresy * osc_param.display_aspect,
|
|
osc_param.playresy, ass.text, 1000)
|
|
end
|
|
|
|
-- called by mpv on every frame
|
|
tick = function()
|
|
if state.marginsREQ == true then
|
|
update_margins()
|
|
state.marginsREQ = false
|
|
end
|
|
|
|
if not state.enabled then return end
|
|
|
|
if state.idle then
|
|
-- render idle message
|
|
msg.trace("idle message")
|
|
local _, _, display_aspect = mp.get_osd_size()
|
|
if display_aspect == 0 then
|
|
return
|
|
end
|
|
local display_h = 360
|
|
local display_w = display_h * display_aspect
|
|
-- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800
|
|
local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140
|
|
local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y)
|
|
|
|
local ass = assdraw.ass_new()
|
|
-- mpv logo
|
|
if user_opts.idlescreen then
|
|
for _, line in ipairs(logo_lines) do
|
|
ass:new_event()
|
|
ass:append(line_prefix .. line)
|
|
end
|
|
end
|
|
|
|
-- Santa hat
|
|
if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then
|
|
for _, line in ipairs(santa_hat_lines) do
|
|
ass:new_event()
|
|
ass:append(line_prefix .. line)
|
|
end
|
|
end
|
|
|
|
if user_opts.idlescreen then
|
|
ass:new_event()
|
|
ass:pos(display_w / 2, icon_y + 65)
|
|
ass:an(8)
|
|
ass:append(locale.idle)
|
|
end
|
|
set_osd(display_w, display_h, ass.text, -1000)
|
|
|
|
if state.showhide_enabled then
|
|
mp.disable_key_bindings("showhide")
|
|
mp.disable_key_bindings("showhide_wc")
|
|
state.showhide_enabled = false
|
|
end
|
|
elseif (state.fullscreen and user_opts.showfullscreen)
|
|
or (not state.fullscreen and user_opts.showwindowed) then
|
|
|
|
-- render the OSC
|
|
render()
|
|
else
|
|
-- Flush OSD
|
|
render_wipe()
|
|
end
|
|
|
|
state.tick_last_time = mp.get_time()
|
|
|
|
if state.anitype ~= nil then
|
|
-- state.anistart can be nil - animation should now start, or it can
|
|
-- be a timestamp when it started. state.idle has no animation.
|
|
if not state.idle and
|
|
(not state.anistart or
|
|
mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000)
|
|
then
|
|
-- animating or starting, or still within 1s past the deadline
|
|
request_tick()
|
|
else
|
|
kill_animation()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- duration is observed for the sole purpose of updating chapter markers
|
|
-- positions. live streams with chapters are very rare, and the update is also
|
|
-- expensive (with request_init), so it's only observed when we have chapters
|
|
-- and the user didn't disable the livemarkers option (update_duration_watch).
|
|
local function on_duration() request_init() end
|
|
|
|
local duration_watched = false
|
|
local function update_duration_watch()
|
|
local want_watch = user_opts.livemarkers and
|
|
(mp.get_property_number("chapters", 0) or 0) > 0 and
|
|
true or false -- ensure it's a boolean
|
|
|
|
if want_watch ~= duration_watched then
|
|
if want_watch then
|
|
mp.observe_property("duration", "native", on_duration)
|
|
else
|
|
mp.unobserve_property(on_duration)
|
|
end
|
|
duration_watched = want_watch
|
|
end
|
|
end
|
|
|
|
local function set_tick_delay(_, display_fps)
|
|
-- may be nil if unavailable or 0 fps is reported
|
|
if not display_fps or not user_opts.tick_delay_follow_display_fps then
|
|
tick_delay = user_opts.tick_delay
|
|
return
|
|
end
|
|
tick_delay = 1 / display_fps
|
|
end
|
|
|
|
mp.register_event("file-loaded", function()
|
|
is_image() -- check if file is an image
|
|
state.new_file_flag = true
|
|
state.file_size_normalized = "Approximating size..."
|
|
check_path_url()
|
|
if user_opts.automatickeyframemode then
|
|
if mp.get_property_number("duration", 0) > user_opts.automatickeyframelimit then
|
|
user_opts.seekbarkeyframes = true
|
|
else
|
|
user_opts.seekbarkeyframes = false
|
|
end
|
|
end
|
|
if user_opts.osc_on_start then
|
|
show_osc()
|
|
end
|
|
end)
|
|
mp.register_event("start-file", request_init)
|
|
mp.observe_property("track-list", "native", request_init)
|
|
mp.observe_property("playlist-count", "native", request_init)
|
|
mp.observe_property("playlist-pos", "native", request_init)
|
|
mp.observe_property("chapter-list", "native", function(_, list)
|
|
list = list or {} -- safety, shouldn't return nil
|
|
table.sort(list, function(a, b) return a.time < b.time end)
|
|
state.chapter_list = list
|
|
update_duration_watch()
|
|
request_init()
|
|
end)
|
|
mp.observe_property("seeking", "native", function(_, seeking)
|
|
if user_opts.seek_resets_hidetimeout then
|
|
reset_timeout()
|
|
end
|
|
|
|
if state.new_file_flag then
|
|
state.new_file_flag = false
|
|
return
|
|
end
|
|
|
|
if seeking and user_opts.osc_on_seek then
|
|
mp.register_event("seek", show_osc) -- show OSC while seeking
|
|
else
|
|
mp.unregister_event(show_osc) -- remove event when seeking stops
|
|
end
|
|
end)
|
|
mp.observe_property("fullscreen", "bool", function(_, val)
|
|
state.fullscreen = val
|
|
state.marginsREQ = true
|
|
adjust_subtitles(state.osc_visible)
|
|
request_init_resize()
|
|
end)
|
|
mp.observe_property("border", "bool", function(_, val)
|
|
state.border = val
|
|
request_init_resize()
|
|
end)
|
|
mp.observe_property("title-bar", "bool", function(_, val)
|
|
state.title_bar = val
|
|
request_init_resize()
|
|
end)
|
|
mp.observe_property("window-maximized", "bool", function(_, val)
|
|
state.maximized = val
|
|
request_init_resize()
|
|
end)
|
|
mp.observe_property("idle-active", "bool", function(_, val)
|
|
state.idle = val
|
|
request_tick()
|
|
end)
|
|
mp.observe_property("display-fps", "number", set_tick_delay)
|
|
mp.observe_property("demuxer-cache-state", "native", cache_state)
|
|
mp.observe_property("vo-configured", "bool", request_tick)
|
|
mp.observe_property("playback-time", "number", request_tick)
|
|
mp.observe_property("osd-dimensions", "native", function()
|
|
-- (we could use the value instead of re-querying it all the time, but then
|
|
-- we might have to worry about property update ordering)
|
|
request_init_resize()
|
|
adjust_subtitles(state.osc_visible)
|
|
end)
|
|
mp.observe_property("osd-scale-by-window", "native", request_init_resize)
|
|
mp.observe_property("touch-pos", "native", handle_touch)
|
|
mp.observe_property("mute", "bool", function(_, val)
|
|
state.mute = val
|
|
request_tick()
|
|
end)
|
|
mp.observe_property("paused-for-cache", "bool", function(_, val) state.buffering = val end)
|
|
-- ensure compatibility with auto looping scripts (eg: a script that sets videos under 2 seconds to loop by default)
|
|
mp.observe_property("loop-file", "bool", function(_, val)
|
|
if (val == nil) then
|
|
state.looping = true
|
|
else
|
|
state.looping = false
|
|
end
|
|
end)
|
|
mp.observe_property("sub-pos", "native", function(_, value)
|
|
if value == nil then return end
|
|
|
|
if state.osc_adjusted_subpos == nil or value ~= state.osc_adjusted_subpos then
|
|
state.user_subpos = value
|
|
end
|
|
end)
|
|
|
|
-- mouse show/hide bindings
|
|
mp.set_key_bindings({
|
|
{"mouse_move", function() process_event("mouse_move", nil) end},
|
|
{"mouse_leave", mouse_leave},
|
|
}, "showhide", "force")
|
|
mp.set_key_bindings({
|
|
{"mouse_move", function() process_event("mouse_move", nil) end},
|
|
{"mouse_leave", mouse_leave},
|
|
}, "showhide_wc", "force")
|
|
do_enable_keybindings()
|
|
|
|
--mouse input bindings
|
|
mp.set_key_bindings({
|
|
{"mbtn_left", function() process_event("mbtn_left", "up") end,
|
|
function() process_event("mbtn_left", "down") end},
|
|
{"shift+mbtn_left", function() process_event("shift+mbtn_left", "up") end,
|
|
function() process_event("shift+mbtn_left", "down") end},
|
|
{"mbtn_right", function() process_event("mbtn_right", "up") end,
|
|
function() process_event("mbtn_right", "down") end},
|
|
{"shift+mbtn_right", function(e) process_event("shift+mbtn_right", "up") end,
|
|
function(e) process_event("shift+mbtn_right", "down") end},
|
|
-- alias to shift_mbtn_left for single-handed mouse use
|
|
{"mbtn_mid", function() process_event("shift+mbtn_left", "up") end,
|
|
function() process_event("shift+mbtn_left", "down") end},
|
|
{"wheel_up", function() process_event("wheel_up", "press") end},
|
|
{"wheel_down", function() process_event("wheel_down", "press") end},
|
|
{"mbtn_left_dbl", "ignore"},
|
|
{"shift+mbtn_left_dbl", "ignore"},
|
|
{"mbtn_right_dbl", "ignore"},
|
|
}, "input", "force")
|
|
mp.enable_key_bindings("input")
|
|
|
|
mp.set_key_bindings({
|
|
{"mbtn_left", function() process_event("mbtn_left", "up") end,
|
|
function() process_event("mbtn_left", "down") end},
|
|
}, "window-controls", "force")
|
|
mp.enable_key_bindings("window-controls")
|
|
|
|
local function always_on(val)
|
|
if state.enabled then
|
|
if val then
|
|
show_osc()
|
|
else
|
|
hide_osc()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- mode can be auto/always/never/cycle
|
|
-- the modes only affect internal variables and not stored on its own.
|
|
local function visibility_mode(mode, no_osd)
|
|
if mode == "cycle" then
|
|
for i, allowed_mode in ipairs(state.visibility_modes) do
|
|
if i == #state.visibility_modes then
|
|
mode = state.visibility_modes[1]
|
|
break
|
|
elseif user_opts.visibility == allowed_mode then
|
|
mode = state.visibility_modes[i + 1]
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if mode == "auto" then
|
|
always_on(false)
|
|
enable_osc(true)
|
|
elseif mode == "always" then
|
|
enable_osc(true)
|
|
always_on(true)
|
|
elseif mode == "never" then
|
|
enable_osc(false)
|
|
else
|
|
msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
|
|
return
|
|
end
|
|
|
|
user_opts.visibility = mode
|
|
mp.set_property_native("user-data/osc/visibility", mode)
|
|
|
|
if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
|
|
mp.osd_message("OSC visibility: " .. mode)
|
|
end
|
|
|
|
-- Reset the input state on a mode change. The input state will be
|
|
-- recalculated on the next render cycle, except in 'never' mode where it
|
|
-- will just stay disabled.
|
|
mp.disable_key_bindings("input")
|
|
mp.disable_key_bindings("window-controls")
|
|
state.input_enabled = false
|
|
|
|
update_margins()
|
|
request_tick()
|
|
end
|
|
|
|
local function idlescreen_visibility(mode, no_osd)
|
|
if mode == "cycle" then
|
|
if user_opts.idlescreen then
|
|
mode = "no"
|
|
else
|
|
mode = "yes"
|
|
end
|
|
end
|
|
|
|
if mode == "yes" then
|
|
user_opts.idlescreen = true
|
|
else
|
|
user_opts.idlescreen = false
|
|
end
|
|
|
|
mp.set_property_native("user-data/osc/idlescreen", user_opts.idlescreen)
|
|
|
|
if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
|
|
mp.osd_message("OSC logo visibility: " .. tostring(mode))
|
|
end
|
|
|
|
request_tick()
|
|
end
|
|
|
|
mp.observe_property("pause", "bool", function(name, enabled)
|
|
pause_state(name, enabled)
|
|
if user_opts.showonpause and user_opts.visibility ~= "never" then
|
|
state.enabled = enabled
|
|
if enabled then
|
|
-- save mode if a temporary change is needed
|
|
if not state.temp_visibility_mode and user_opts.visibility ~= "always" then
|
|
state.temp_visibility_mode = user_opts.visibility
|
|
end
|
|
|
|
if user_opts.keeponpause then
|
|
-- set visibility to "always" temporarily
|
|
visibility_mode("always", true)
|
|
else
|
|
show_osc()
|
|
end
|
|
else
|
|
-- restore mode if it was changed temporarily
|
|
if state.temp_visibility_mode then
|
|
visibility_mode(state.temp_visibility_mode, true)
|
|
state.temp_visibility_mode = nil
|
|
else
|
|
-- respect "always" mode on unpause
|
|
visibility_mode(user_opts.visibility, true)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
mp.register_script_message("osc-visibility", visibility_mode)
|
|
mp.register_script_message("osc-show", show_osc)
|
|
mp.register_script_message("osc-hide", function()
|
|
if user_opts.visibility == "auto" then
|
|
osc_visible(false)
|
|
end
|
|
end)
|
|
mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
|
|
mp.add_key_binding(nil, "progress-toggle", function()
|
|
user_opts.persistentprogress = not user_opts.persistentprogress
|
|
state.persistent_progress_toggle = user_opts.persistentprogress
|
|
request_init()
|
|
end)
|
|
mp.register_script_message("osc-idlescreen", idlescreen_visibility)
|
|
mp.register_script_message("thumbfast-info", function(json)
|
|
local data = utils.parse_json(json)
|
|
if type(data) ~= "table" or not data.width or not data.height then
|
|
msg.error("thumbfast-info: received json didn't produce a table with thumbnail information")
|
|
else
|
|
thumbfast = data
|
|
end
|
|
end)
|
|
|
|
-- validate string type user options
|
|
local function validate_user_opts()
|
|
if user_opts.window_top_bar ~= "auto" and
|
|
user_opts.window_top_bar ~= "yes" and
|
|
user_opts.window_top_bar ~= "no" then
|
|
msg.warn("window_top_bar cannot be '" .. user_opts.window_top_bar .. "'. Ignoring.")
|
|
user_opts.window_top_bar = "auto"
|
|
end
|
|
|
|
if user_opts.seek_handle_size < 0 then
|
|
msg.warn("seek_handle_size must be 0 or higher. Setting it to 0 (minimum).")
|
|
user_opts.seek_handle_size = 0
|
|
end
|
|
|
|
if user_opts.volume_control_type ~= "linear" and
|
|
user_opts.volume_control_type ~= "logarithmic" then
|
|
msg.warn("volumecontrol cannot be '" .. user_opts.volume_control_type .. "'. Ignoring.")
|
|
user_opts.volume_control_type = "linear"
|
|
end
|
|
|
|
if user_opts.screenshot_flag ~= "subtitles" and
|
|
user_opts.screenshot_flag ~= "video" and
|
|
user_opts.screenshot_flag ~= "window" and
|
|
user_opts.screenshot_flag ~= "each-frame" and
|
|
user_opts.screenshot_flag ~= "subtitles+each-frame" and
|
|
user_opts.screenshot_flag ~= "video+each-frame" and
|
|
user_opts.screenshot_flag ~= "window+each-frame" then
|
|
msg.warn("screenshot_flag cannot be '" .. user_opts.screenshot_flag .. "'. Ignoring.")
|
|
user_opts.screenshot_flag = "subtitles"
|
|
end
|
|
|
|
if not language[user_opts.language] then
|
|
msg.warn("language '" .. user_opts.language .. "' not found. Ignoring.")
|
|
user_opts.language = "en"
|
|
if not language["en"] then
|
|
msg.warn("ERROR: can't find the default 'en' language or the one set by user_opts.")
|
|
end
|
|
end
|
|
|
|
local colors = {
|
|
user_opts.osc_color, user_opts.seekbarfg_color, user_opts.seekbarbg_color,
|
|
user_opts.title_color, user_opts.time_color, user_opts.side_buttons_color,
|
|
user_opts.middle_buttons_color, user_opts.playpause_color, user_opts.window_title_color,
|
|
user_opts.window_controls_color, user_opts.held_element_color, user_opts.thumbnail_border_color,
|
|
user_opts.chapter_title_color, user_opts.seekbar_cache_color, user_opts.hover_effect_color,
|
|
user_opts.windowcontrols_close_hover, user_opts.windowcontrols_max_hover, user_opts.windowcontrols_min_hover,
|
|
user_opts.cache_info_color, user_opts.thumbnail_border_outline,
|
|
}
|
|
|
|
for _, color in pairs(colors) do
|
|
if color:find("^#%x%x%x%x%x%x$") == nil then
|
|
msg.warn("'" .. color .. "' is not a valid color")
|
|
end
|
|
end
|
|
|
|
for str in string.gmatch(user_opts.visibility_modes, "([^_]+)") do
|
|
if str ~= "auto" and str ~= "always" and str ~= "never" then
|
|
msg.warn("Ignoring unknown visibility mode '" .. str .."' in list")
|
|
else
|
|
table.insert(state.visibility_modes, str)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- read options from config and command-line
|
|
opt.read_options(user_opts, "modernz", function(changed)
|
|
validate_user_opts()
|
|
set_osc_locale()
|
|
set_icon_theme()
|
|
set_osc_styles()
|
|
set_time_styles(changed.timetotal, changed.timems)
|
|
if changed.tick_delay or changed.tick_delay_follow_display_fps then
|
|
set_tick_delay("display_fps", mp.get_property_number("display_fps"))
|
|
end
|
|
request_tick()
|
|
visibility_mode(user_opts.visibility, true)
|
|
update_duration_watch()
|
|
request_init()
|
|
end)
|
|
|
|
validate_user_opts()
|
|
set_osc_locale()
|
|
set_icon_theme()
|
|
set_osc_styles()
|
|
set_time_styles(true, true)
|
|
set_tick_delay("display_fps", mp.get_property_number("display_fps"))
|
|
visibility_mode(user_opts.visibility, true)
|
|
update_duration_watch()
|
|
|
|
set_virt_mouse_area(0, 0, 0, 0, "input")
|
|
set_virt_mouse_area(0, 0, 0, 0, "window-controls")
|
|
set_virt_mouse_area(0, 0, 0, 0, "window-controls-title")
|