aboutsummaryrefslogtreecommitdiff
path: root/mpv/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'mpv/scripts')
-rw-r--r--mpv/scripts/autosub.lua254
-rw-r--r--mpv/scripts/autosubsync.lua44
-rw-r--r--mpv/scripts/modules.lua3
-rw-r--r--mpv/scripts/morden.lua2041
-rw-r--r--mpv/scripts/uosc.lua3230
5 files changed, 2044 insertions, 3528 deletions
diff --git a/mpv/scripts/autosub.lua b/mpv/scripts/autosub.lua
deleted file mode 100644
index 56d49fa..0000000
--- a/mpv/scripts/autosub.lua
+++ /dev/null
@@ -1,254 +0,0 @@
---=============================================================================
--->> SUBLIMINAL PATH:
---=============================================================================
--- This script uses Subliminal to download subtitles,
--- so make sure to specify your system's Subliminal location below:
-local subliminal = '/usr/bin/subliminal'
---=============================================================================
--->> SUBTITLE LANGUAGE:
---=============================================================================
--- Specify languages in this order:
--- { 'language name', 'ISO-639-1', 'ISO-639-2' } !
--- (See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
-local languages = {
--- If subtitles are found for the first language,
--- other languages will NOT be downloaded,
--- so put your preferred language first:
- { 'English', 'en', 'eng' },
- { 'Dutch', 'nl', 'dut' },
--- { 'Spanish', 'es', 'spa' },
--- { 'French', 'fr', 'fre' },
--- { 'German', 'de', 'ger' },
--- { 'Italian', 'it', 'ita' },
--- { 'Portuguese', 'pt', 'por' },
--- { 'Polish', 'pl', 'pol' },
--- { 'Russian', 'ru', 'rus' },
--- { 'Chinese', 'zh', 'chi' },
--- { 'Arabic', 'ar', 'ara' },
-}
---=============================================================================
--->> PROVIDER LOGINS:
---=============================================================================
--- These are completely optional and not required
--- for the functioning of the script!
--- If you use any of these services, simply uncomment it
--- and replace 'USERNAME' and 'PASSWORD' with your own:
-local logins = {
--- { '--addic7ed', 'USERNAME', 'PASSWORD' },
--- { '--legendastv', 'USERNAME', 'PASSWORD' },
--- { '--opensubtitles', 'USERNAME', 'PASSWORD' },
--- { '--subscenter', 'USERNAME', 'PASSWORD' },
-}
---=============================================================================
--->> ADDITIONAL OPTIONS:
---=============================================================================
-local bools = {
- auto = true, -- Automatically download subtitles, no hotkeys required
- debug = false, -- Use `--debug` in subliminal command for debug output
- force = true, -- Force download; will overwrite existing subtitle files
- utf8 = true, -- Save all subtitle files as UTF-8
-}
-local excludes = {
- -- Movies with a path containing any of these strings/paths
- -- will be excluded from auto-downloading subtitles.
- -- Full paths are also allowed, e.g.:
- -- '/home/david/Videos',
- 'no-subs-dl',
-}
-local includes = {
- -- If anything is defined here, only the movies with a path
- -- containing any of these strings/paths will auto-download subtitles.
- -- Full paths are also allowed, e.g.:
- -- '/home/david/Videos',
-}
---=============================================================================
-local utils = require 'mp.utils'
-
-
--- Download function: download the best subtitles in most preferred language
-function download_subs(language)
- language = language or languages[1]
- log('Searching ' .. language[1] .. ' subtitles ...', 30)
-
- -- Build the `subliminal` command, starting with the executable:
- local table = { args = { subliminal } }
- local a = table.args
-
- for _, login in ipairs(logins) do
- a[#a + 1] = login[1]
- a[#a + 1] = login[2]
- a[#a + 1] = login[3]
- end
- if bools.debug then
- -- To see `--debug` output start MPV from the terminal!
- a[#a + 1] = '--debug'
- end
-
- a[#a + 1] = 'download'
- if bools.force then
- a[#a + 1] = '-f'
- end
- if bools.utf8 then
- a[#a + 1] = '-e'
- a[#a + 1] = 'utf-8'
- end
-
- a[#a + 1] = '-l'
- a[#a + 1] = language[2]
- a[#a + 1] = '-d'
- a[#a + 1] = directory
- a[#a + 1] = filename --> Subliminal command ends with the movie filename.
-
- local result = utils.subprocess(table)
-
- if string.find(result.stdout, 'Downloaded 1 subtitle') then
- -- When multiple external files are present,
- -- always activate the most recently downloaded:
- mp.set_property('slang', language[2])
- -- Subtitles are downloaded successfully, so rescan to activate them:
- mp.commandv('rescan_external_files')
- log(language[1] .. ' subtitles ready!')
- return true
- else
- log('No ' .. language[1] .. ' subtitles found\n')
- return false
- end
-end
-
--- Manually download second language subs by pressing 'n':
-function download_subs2()
- download_subs(languages[2])
-end
-
--- Control function: only download if necessary
-function control_downloads()
- -- Make MPV accept external subtitle files with language specifier:
- mp.set_property('sub-auto', 'fuzzy')
- -- Set subtitle language preference:
- mp.set_property('slang', languages[1][2])
- mp.msg.warn('Reactivate external subtitle files:')
- mp.commandv('rescan_external_files')
- directory, filename = utils.split_path(mp.get_property('path'))
-
- if not autosub_allowed() then
- return
- end
-
- sub_tracks = {}
- for _, track in ipairs(mp.get_property_native('track-list')) do
- if track['type'] == 'sub' then
- sub_tracks[#sub_tracks + 1] = track
- end
- end
- if bools.debug then -- Log subtitle properties to terminal:
- for _, track in ipairs(sub_tracks) do
- mp.msg.warn('Subtitle track', track['id'], ':\n{')
- for k, v in pairs(track) do
- if type(v) == 'string' then v = '"' .. v .. '"' end
- mp.msg.warn(' "' .. k .. '":', v)
- end
- mp.msg.warn('}\n')
- end
- end
-
- for _, language in ipairs(languages) do
- if should_download_subs_in(language) then
- if download_subs(language) then return end -- Download successful!
- else return end -- No need to download!
- end
- log('No subtitles were found')
-end
-
--- Check if subtitles should be auto-downloaded:
-function autosub_allowed()
- local duration = tonumber(mp.get_property('duration'))
- local active_format = mp.get_property('file-format')
-
- if not bools.auto then
- mp.msg.warn('Automatic downloading disabled!')
- return false
- elseif duration < 900 then
- mp.msg.warn('Video is less than 15 minutes\n' ..
- '=> NOT auto-downloading subtitles')
- return false
- elseif directory:find('^http') then
- mp.msg.warn('Automatic subtitle downloading is disabled for web streaming')
- return false
- elseif active_format:find('^cue') then
- mp.msg.warn('Automatic subtitle downloading is disabled for cue files')
- return false
- else
- local not_allowed = {'aiff', 'ape', 'flac', 'mp3', 'ogg', 'wav', 'wv'}
-
- for _, file_format in pairs(not_allowed) do
- if file_format == active_format then
- mp.msg.warn('Automatic subtitle downloading is disabled for audio files')
- return false
- end
- end
-
- for _, exclude in pairs(excludes) do
- local escaped_exclude = exclude:gsub('%W','%%%0')
- local excluded = directory:find(escaped_exclude)
-
- if excluded then
- mp.msg.warn('This path is excluded from auto-downloading subs')
- return false
- end
- end
-
- for i, include in ipairs(includes) do
- local escaped_include = include:gsub('%W','%%%0')
- local included = directory:find(escaped_include)
-
- if included then break
- elseif i == #includes then
- mp.msg.warn('This path is not included for auto-downloading subs')
- return false
- end
- end
- end
-
- return true
-end
-
--- Check if subtitles should be downloaded in this language:
-function should_download_subs_in(language)
- for i, track in ipairs(sub_tracks) do
- local subtitles = track['external'] and
- 'subtitle file' or 'embedded subtitles'
-
- if not track['lang'] and (track['external'] or not track['title'])
- and i == #sub_tracks then
- local status = track['selected'] and ' active' or ' present'
- log('Unknown ' .. subtitles .. status)
- mp.msg.warn('=> NOT downloading new subtitles')
- return false -- Don't download if 'lang' key is absent
- elseif track['lang'] == language[3] or track['lang'] == language[2] or
- (track['title'] and track['title']:lower():find(language[3])) then
- if not track['selected'] then
- mp.set_property('sid', track['id'])
- log('Enabled ' .. language[1] .. ' ' .. subtitles .. '!')
- else
- log(language[1] .. ' ' .. subtitles .. ' active')
- end
- mp.msg.warn('=> NOT downloading new subtitles')
- return false -- The right subtitles are already present
- end
- end
- mp.msg.warn('No ' .. language[1] .. ' subtitles were detected\n' ..
- '=> Proceeding to download:')
- return true
-end
-
--- Log function: log to both terminal and MPV OSD (On-Screen Display)
-function log(string, secs)
- secs = secs or 2.5 -- secs defaults to 2.5 when secs parameter is absent
- mp.msg.warn(string) -- This logs to the terminal
- mp.osd_message(string, secs) -- This logs to MPV screen
-end
-
-
-mp.add_key_binding('b', 'download_subs', download_subs)
-mp.add_key_binding('n', 'download_subs2', download_subs2)
-mp.register_event('file-loaded', control_downloads)
diff --git a/mpv/scripts/autosubsync.lua b/mpv/scripts/autosubsync.lua
deleted file mode 100644
index fb3a6c3..0000000
--- a/mpv/scripts/autosubsync.lua
+++ /dev/null
@@ -1,44 +0,0 @@
--- default keybinding: n
--- add the following to your input.conf to change the default keybinding:
--- keyname script_binding auto_sync_subs
-local utils = require 'mp.utils'
-
-function display_error()
- mp.msg.warn("Subtitle synchronization failed: ")
- mp.osd_message("Subtitle synchronization failed")
-end
-
--- Courtesy of https://stackoverflow.com/questions/4990990/check-if-a-file-exists-with-lua
-function file_exists(filepath)
- local f=io.open(filepath,"r")
- if f~=nil then io.close(f) return true else return false end
-end
-
-function sync_sub_fn()
- path = mp.get_property("path")
- srt_path = string.gsub(path, "%.%w+$", ".srt")
- if file_exists(srt_path)==false then
- mp.msg.warn("Couldn't find",srt_path)
- display_error()
- do return end
- end
- subsync = "/home/user/.local/bin/ffsubsync" -- use 'which ffsubsync' to find the path
- t = {}
- t.args = {subsync, path, "-i",srt_path,"-o",srt_path}
-
- mp.osd_message("Sync subtitle...")
- mp.msg.info("Starting ffsubsync...")
- res = utils.subprocess(t)
- if res.error == nil then
- if mp.commandv("sub_add", srt_path) then
- mp.msg.info("Subtitle updated")
- mp.osd_message("Subtitle at'" .. srt_path .. "' synchronized")
- else
- display_error()
- end
- else
- display_error()
- end
-end
-
-mp.add_key_binding("n", "auto_sync_subs", sync_sub_fn)
diff --git a/mpv/scripts/modules.lua b/mpv/scripts/modules.lua
new file mode 100644
index 0000000..703f372
--- /dev/null
+++ b/mpv/scripts/modules.lua
@@ -0,0 +1,3 @@
+local mpv_config_dir_path = require("mp").command_native({"expand-path", "~~/"})
+function load(relative_path) dofile(mpv_config_dir_path .. "/script_modules/" .. relative_path) end
+load("mpvSockets/mpvSockets.lua")
diff --git a/mpv/scripts/morden.lua b/mpv/scripts/morden.lua
new file mode 100644
index 0000000..b62c6e2
--- /dev/null
+++ b/mpv/scripts/morden.lua
@@ -0,0 +1,2041 @@
+-- by maoiscat
+-- email:valarmor@163.com
+-- https://github.com/maoiscat/mpv-osc-morden
+
+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
+-- may change them in osc.conf
+local user_opts = {
+ showwindowed = true, -- show OSC when windowed?
+ showfullscreen = true, -- show OSC when fullscreen?
+ scalewindowed = 1, -- scaling of the controller when windowed
+ scalefullscreen = 1, -- scaling of the controller when fullscreen
+ scaleforcedwindow = 2, -- scaling when rendered on a forced window
+ vidscale = false, -- scale the controller with the video?
+ hidetimeout = 1000, -- duration in ms until the OSC hides if no
+ -- mouse movement. enforced non-negative for the
+ -- user, but internally negative is 'always-on'.
+ fadeduration = 500, -- duration of fade out in ms, 0 = no fade
+ minmousemove = 3, -- minimum amount of pixels the mouse has to
+ -- move between ticks to make the OSC show up
+ iamaprogrammer = false, -- use native mpv values and disable OSC
+ -- internal track list management (and some
+ -- functions that depend on it)
+ font = 'mpv-osd-symbols', -- default osc font
+ seekbarhandlesize = 1.0, -- size ratio of the slider handle, range 0 ~ 1
+ seekrange = true, -- show seekrange overlay
+ seekrangealpha = 128, -- transparency of seekranges
+ seekbarkeyframes = true, -- use keyframes when dragging the seekbar
+ title = '${media-title}', -- string compatible with property-expansion
+ -- to be shown as OSC title
+ showtitle = true, -- show title and no hide timeout on pause
+ timetotal = true, -- display total time instead of remaining time?
+ visibility = 'auto', -- only used at init to set visibility_mode(...)
+ windowcontrols = 'auto', -- whether to show window controls
+ language = 'eng', -- eng=English, chs=Chinese
+}
+
+-- Localization
+local language = {
+ ['eng'] = {
+ welcome = '{\\fs24\\1c&H0&\\3c&HFFFFFF&}Drop files or URLs to play here.', -- this text appears when mpv starts
+ off = 'OFF',
+ na = 'n/a',
+ none = 'none',
+ video = 'Video',
+ audio = 'Audio',
+ subtitle = 'Subtitle',
+ available = 'Available ',
+ track = ' Tracks:',
+ playlist = 'Playlist',
+ nolist = 'Empty playlist.',
+ chapter = 'Chapter',
+ nochapter = 'No chapters.',
+ },
+ ['chs'] = {
+ welcome = '{\\1c&H00\\bord0\\fs30\\fn微软雅黑 light\\fscx125}MPV{\\fscx100} 播放器', -- this text appears when mpv starts
+ off = '关闭',
+ na = 'n/a',
+ none = '无',
+ video = '视频',
+ audio = '音频',
+ subtitle = '字幕',
+ available = '可选',
+ track = ':',
+ playlist = '播放列表',
+ nolist = '无列表信息',
+ chapter = '章节',
+ nochapter = '无章节信息',
+ }
+}
+-- read options from config and command-line
+opt.read_options(user_opts, 'osc', function(list) update_options(list) end)
+-- apply lang opts
+local texts = language[user_opts.language]
+local osc_param = { -- calculated by osc_init()
+ playresy = 0, -- canvas size Y
+ playresx = 0, -- canvas size X
+ display_aspect = 1,
+ unscaled_y = 0,
+ areas = {},
+}
+
+local osc_styles = {
+ TransBg = '{\\blur100\\bord140\\1c&H000000&\\3c&H000000&}',
+ SeekbarBg = '{\\blur0\\bord0\\1c&HFFFFFF&}',
+ SeekbarFg = '{\\blur1\\bord1\\1c&HE39C42&}',
+ Ctrl1 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs36\\fnmaterial-design-iconic-font}',
+ Ctrl2 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}',
+ Ctrl3 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs24\\fnmaterial-design-iconic-font}',
+ Time = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&H000000&\\fs17\\fn' .. user_opts.font .. '}',
+ Tooltip = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H000000&\\fs18\\fn' .. user_opts.font .. '}',
+ Title = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs48\\q2\\fn' .. user_opts.font .. '}',
+ WinCtrl = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs20\\fnmpv-osd-symbols}',
+ elementDown = '{\\1c&H999999&}',
+}
+
+-- internal states, do not touch
+local state = {
+ showtime, -- time of last invocation (last mouse move)
+ osc_visible = false,
+ anistart, -- time when the animation started
+ anitype, -- current type of animation
+ animation, -- 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
+ rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
+ mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
+ initREQ = false, -- is a re-init request pending?
+ last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
+ mouse_in_window = false,
+ message_text,
+ message_hide_timer,
+ 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,
+ dmx_cache = 0,
+ border = true,
+ maximized = false,
+ osd = mp.create_osd_overlay('ass-events'),
+ lastvisibility = user_opts.visibility, -- save last visibility on pause if showtitle
+}
+
+local window_control_box_width = 138
+local tick_delay = 0.03
+
+--
+-- Helperfunctions
+--
+
+function set_osd(res_x, res_y, text)
+ 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 = 1000
+ state.osd:update()
+end
+
+-- scale factor for translating between real and virtual ASS coordinates
+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)
+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
+
+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
+
+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
+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
+
+function get_hitbox_coords_geo(geometry)
+ return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
+ geometry.w, geometry.h)
+end
+
+function get_element_hitbox(element)
+ return element.hitbox.x1, element.hitbox.y1,
+ element.hitbox.x2, element.hitbox.y2
+end
+
+function mouse_hit(element)
+ return mouse_hit_coords(get_element_hitbox(element))
+end
+
+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
+
+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
+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
+function get_slider_value_at(element, glob_pos)
+
+ 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
+
+-- get value at current mouse position
+function get_slider_value(element)
+ return get_slider_value_at(element, get_virt_mouse_pos())
+end
+
+function countone(val)
+ if not (user_opts.iamaprogrammer) then
+ val = val + 1
+ end
+ return val
+end
+
+-- multiplies two alpha values, formular can probably be improved
+function mult_alpha(alphaA, alphaB)
+ return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
+end
+
+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
+
+function ass_append_alpha(ass, alpha, modifier)
+ local ar = {}
+
+ for ai, av in pairs(alpha) do
+ av = mult_alpha(av, modifier)
+ if state.animation then
+ av = mult_alpha(av, state.animation)
+ 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
+
+function ass_draw_cir_cw(ass, x, y, r)
+ ass:round_rect_cw(x-r, y-r, x+r, y+r, r)
+end
+
+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
+
+function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
+ if hexagon then
+ ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
+ else
+ ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
+ end
+end
+
+
+--
+-- Tracklist Management
+--
+
+local nicetypes = {video = texts.video, audio = texts.audio, sub = texts.subtitle}
+
+-- updates the OSC internal playlists, should be run each time the track-layout changes
+function update_tracklist()
+ local tracktable = mp.get_property_native('track-list', {})
+
+ -- by osc_id
+ tracks_osc = {}
+ tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
+ -- by mpv_id
+ tracks_mpv = {}
+ tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
+ for n = 1, #tracktable do
+ if not (tracktable[n].type == 'unknown') then
+ local type = tracktable[n].type
+ local mpv_id = tonumber(tracktable[n].id)
+
+ -- by osc_id
+ table.insert(tracks_osc[type], tracktable[n])
+
+ -- by mpv_id
+ tracks_mpv[type][mpv_id] = tracktable[n]
+ tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
+ end
+ end
+end
+
+-- return a nice list of tracks of the given type (video, audio, sub)
+function get_tracklist(type)
+ local msg = texts.available .. nicetypes[type] .. texts.track
+ if #tracks_osc[type] == 0 then
+ msg = msg .. texts.none
+ else
+ for n = 1, #tracks_osc[type] do
+ local track = tracks_osc[type][n]
+ local lang, title, selected = 'unknown', '', '○'
+ if not(track.lang == nil) then lang = track.lang end
+ if not(track.title == nil) then title = track.title end
+ if (track.id == tonumber(mp.get_property(type))) then
+ selected = '●'
+ end
+ msg = msg..'\n'..selected..' '..n..': ['..lang..'] '..title
+ end
+ end
+ return msg
+end
+
+-- relatively change the track of given <type> by <next> tracks
+ --(+1 -> next, -1 -> previous)
+function set_track(type, next)
+ local current_track_mpv, current_track_osc
+ if (mp.get_property(type) == 'no') then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.get_property(type))
+ current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
+ end
+ local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
+ local new_track_mpv
+ if new_track_osc == 0 then
+ new_track_mpv = 'no'
+ else
+ new_track_mpv = tracks_osc[type][new_track_osc].id
+ end
+
+ mp.commandv('set', type, new_track_mpv)
+
+-- if (new_track_osc == 0) then
+-- show_message(nicetypes[type] .. ' Track: none')
+-- else
+-- show_message(nicetypes[type] .. ' Track: '
+-- .. new_track_osc .. '/' .. #tracks_osc[type]
+-- .. ' ['.. (tracks_osc[type][new_track_osc].lang or 'unknown') ..'] '
+-- .. (tracks_osc[type][new_track_osc].title or ''))
+-- end
+end
+
+-- get the currently selected track of <type>, OSC-style counted
+function get_track(type)
+ local track = mp.get_property(type)
+ if track ~= 'no' and track ~= nil then
+ local tr = tracks_mpv[type][tonumber(track)]
+ if tr then
+ return tr.osc_id
+ end
+ end
+ return 0
+end
+
+-- WindowControl helpers
+function window_controls_enabled()
+ val = user_opts.windowcontrols
+ if val == 'auto' then
+ return (not state.border) or state.fullscreen
+ else
+ return val ~= 'no'
+ end
+end
+
+--
+-- Element Management
+--
+
+local elements = {}
+
+function prepare_elements()
+
+ -- remove elements without layout or invisble
+ local elements2 = {}
+ for n, element in pairs(elements) do
+ if not (element.layout == nil) and (element.visible) then
+ table.insert(elements2, element)
+ end
+ end
+ elements = elements2
+
+ 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.seekbarhandlesize * elem_geo.h / 2
+ 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 not (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
+ --top
+ if (slider_lo.nibbles_top) 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)
+ end
+ --bottom
+ if (slider_lo.nibbles_bottom) 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)
+ 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] = 136
+ element.eventresponder = nil
+ end
+ end
+end
+
+--
+-- Element Rendering
+--
+function render_elements(master_ass)
+
+ 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 not (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.elementDown)
+ 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 not (element.type == 'button') then
+ elem_ass:merge(element.static_ass)
+ end
+
+ if (element.type == 'slider') 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
+ -- draw pos marker
+ local pos = element.slider.posF()
+ local seekRanges = element.slider.seekRangesF()
+ local rh = user_opts.seekbarhandlesize * elem_geo.h / 2 -- Handle radius
+ local xp
+
+ if pos then
+ xp = get_slider_ele_pos_for(element, pos)
+ ass_draw_cir_cw(elem_ass, xp, elem_geo.h/2, rh)
+ elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap)
+ end
+
+ if seekRanges then
+ elem_ass:draw_stop()
+ elem_ass:merge(element.style_ass)
+ ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
+ elem_ass:merge(element.static_ass)
+
+ for _,range in pairs(seekRanges) do
+ local pstart = get_slider_ele_pos_for(element, range['start'])
+ local pend = get_slider_ele_pos_for(element, range['end'])
+ elem_ass:rect_cw(pstart - rh, slider_lo.gap, pend + rh, elem_geo.h - slider_lo.gap)
+ end
+ end
+
+ elem_ass:draw_stop()
+
+ -- add tooltip
+ if not (element.slider.tooltipF == nil) then
+ if mouse_hit(element) 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
+ else
+ ty = element.hitbox.y1 + elem_geo.h/2
+ 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
+
+ -- 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)
+ end
+ end
+
+ elseif (element.type == 'button') then
+
+ local buttontext
+ if type(element.content) == 'function' then
+ buttontext = element.content() -- function objects
+ elseif not (element.content == nil) then
+ buttontext = element.content -- text objects
+ end
+
+ buttontext = buttontext:gsub(':%((.?.?.?)%) unknown ', ':%(%1%)') --gsub('%) unknown %(\'', '')
+
+ local maxchars = element.layout.button.maxchars
+ -- 认为1个中文字符约等于1.5个英文字符
+ -- local charcount = buttontext:len()- (buttontext:len()-select(2, buttontext:gsub('[^\128-\193]', '')))/1.5
+ local charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
+ if not (maxchars == nil) and (charcount > maxchars) then
+ local limit = math.max(0, maxchars - 3)
+ if (charcount > limit) then
+ while (charcount > limit) do
+ buttontext = buttontext:gsub('.[\128-\191]*$', '')
+ charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
+ end
+ buttontext = buttontext .. '...'
+ end
+ end
+
+ elem_ass:append(buttontext)
+
+ -- add tooltip
+ if not (element.tooltipF == nil) and element.enabled then
+ if mouse_hit(element) then
+ local tooltiplabel = element.tooltipF
+ local an = 1
+ local ty = element.hitbox.y1
+ local tx = get_virt_mouse_pos()
+
+ if ty < osc_param.playresy / 2 then
+ ty = element.hitbox.y2
+ an = 7
+ end
+
+ -- tooltip label
+ if type(element.tooltipF) == 'function' then
+ tooltiplabel = element.tooltipF()
+ else
+ tooltiplabel = element.tooltipF
+ end
+ elem_ass:new_event()
+ 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
+
+--
+-- Message display
+--
+
+-- pos is 1 based
+function limited_list(prop, pos)
+ local proplist = mp.get_property_native(prop, {})
+ local count = #proplist
+ if count == 0 then
+ return count, proplist
+ end
+
+ local fs = tonumber(mp.get_property('options/osd-font-size'))
+ local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
+ if max % 2 == 0 then
+ max = max - 1
+ end
+ local delta = math.ceil(max / 2) - 1
+ local begi = math.max(math.min(pos - delta, count - max + 1), 1)
+ local endi = math.min(begi + max - 1, count)
+
+ local reslist = {}
+ for i=begi, endi do
+ local item = proplist[i]
+ item.current = (i == pos) and true or nil
+ table.insert(reslist, item)
+ end
+ return count, reslist
+end
+
+function get_playlist()
+ local pos = mp.get_property_number('playlist-pos', 0) + 1
+ local count, limlist = limited_list('playlist', pos)
+ if count == 0 then
+ return texts.nolist
+ end
+
+ local message = string.format(texts.playlist .. ' [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local title = v.title
+ local _, filename = utils.split_path(v.filename)
+ if title == nil then
+ title = filename
+ end
+ message = string.format('%s %s %s\n', message,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function get_chapterlist()
+ local pos = mp.get_property_number('chapter', 0) + 1
+ local count, limlist = limited_list('chapter-list', pos)
+ if count == 0 then
+ return texts.nochapter
+ end
+
+ local message = string.format(texts.chapter.. ' [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local time = mp.format_time(v.time)
+ local title = v.title
+ if title == nil then
+ title = string.format(texts.chapter .. ' %02d', i)
+ end
+ message = string.format('%s[%s] %s %s\n', message, time,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function show_message(text, duration)
+
+ --print('text: '..text..' duration: ' .. duration)
+ if duration == nil then
+ duration = tonumber(mp.get_property('options/osd-duration')) / 1000
+ elseif not type(duration) == 'number' then
+ print('duration: ' .. duration)
+ end
+
+ -- cut the text short, otherwise the following functions
+ -- may slow down massively on huge input
+ text = string.sub(text, 0, 4000)
+
+ -- replace actual linebreaks with ASS linebreaks
+ text = string.gsub(text, '\n', '\\N')
+
+ state.message_text = text
+
+ if not state.message_hide_timer then
+ state.message_hide_timer = mp.add_timeout(0, request_tick)
+ end
+ state.message_hide_timer:kill()
+ state.message_hide_timer.timeout = duration
+ state.message_hide_timer:resume()
+ request_tick()
+end
+
+function render_message(ass)
+ if state.message_hide_timer and state.message_hide_timer:is_enabled() and
+ state.message_text
+ then
+ local _, lines = string.gsub(state.message_text, '\\N', '')
+
+ local fontsize = tonumber(mp.get_property('options/osd-font-size'))
+ local outline = tonumber(mp.get_property('options/osd-border-size'))
+ local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
+ local counterscale = osc_param.playresy / osc_param.unscaled_y
+
+ fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
+ outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
+
+ local style = '{\\bord' .. outline .. '\\fs' .. fontsize .. '}'
+
+
+ ass:new_event()
+ ass:append(style .. state.message_text)
+ else
+ state.message_text = nil
+ end
+end
+
+--
+-- Initialisation and Layout
+--
+
+function new_element(name, type)
+ elements[name] = {}
+ elements[name].type = type
+
+ -- 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}}
+ end
+
+
+ return elements[name]
+end
+
+function add_layout(name)
+ if not (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,
+ }
+ elseif (elements[name].type == 'slider') then
+ -- slider defaults
+ elements[name].layout.slider = {
+ border = 1,
+ gap = 1,
+ nibbles_top = true,
+ nibbles_bottom = true,
+ adjust_tooltip = true,
+ tooltip_style = '',
+ tooltip_an = 2,
+ alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
+ }
+ 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
+function window_controls()
+ local wc_geo = {
+ x = 0,
+ y = 32,
+ an = 1,
+ w = osc_param.playresx,
+ h = 32,
+ }
+
+ local controlbox_w = window_control_box_width
+ local titlebox_w = wc_geo.w - controlbox_w
+
+ -- Default alignment is 'right'
+ local controlbox_left = wc_geo.w - controlbox_w
+ local titlebox_left = wc_geo.x
+ local titlebox_right = wc_geo.w - controlbox_w
+
+ add_area('window-controls',
+ get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
+ controlbox_w, wc_geo.h))
+
+ local lo
+
+ local button_y = wc_geo.y - (wc_geo.h / 2)
+ local first_geo =
+ {x = controlbox_left + 27, y = button_y, an = 5, w = 40, h = wc_geo.h}
+ local second_geo =
+ {x = controlbox_left + 69, y = button_y, an = 5, w = 40, h = wc_geo.h}
+ local third_geo =
+ {x = controlbox_left + 115, y = button_y, an = 5, w = 40, h = wc_geo.h}
+
+ -- Window control buttons use symbols in the custom mpv osd font
+ -- because the official unicode codepoints are sufficiently
+ -- exotic that a system might lack an installed font with them,
+ -- and libass will complain that they are not present in the
+ -- default font, even if another font with them is available.
+
+ -- Close: ??
+ ne = new_element('close', 'button')
+ ne.content = '\238\132\149'
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('quit') end
+ lo = add_layout('close')
+ lo.geometry = third_geo
+ lo.style = osc_styles.WinCtrl
+ lo.alpha[3] = 0
+
+ -- Minimize: ??
+ ne = new_element('minimize', 'button')
+ ne.content = '\\n\238\132\146'
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('cycle', 'window-minimized') end
+ lo = add_layout('minimize')
+ lo.geometry = first_geo
+ lo.style = osc_styles.WinCtrl
+ lo.alpha[3] = 0
+
+ -- Maximize: ?? /??
+ ne = new_element('maximize', 'button')
+ if state.maximized or state.fullscreen then
+ ne.content = '\238\132\148'
+ else
+ ne.content = '\238\132\147'
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function ()
+ if state.fullscreen then
+ mp.commandv('cycle', 'fullscreen')
+ else
+ mp.commandv('cycle', 'window-maximized')
+ end
+ end
+ lo = add_layout('maximize')
+ lo.geometry = second_geo
+ lo.style = osc_styles.WinCtrl
+ lo.alpha[3] = 0
+end
+
+--
+-- Layouts
+--
+
+local layouts = {}
+
+-- Default layout
+layouts = function ()
+
+ local osc_geo = {w, h}
+
+ osc_geo.w = osc_param.playresx
+ osc_geo.h = 180
+
+ -- 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, 104))
+
+ -- 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
+
+ new_element('TransBg', 'box')
+ lo = add_layout('TransBg')
+ lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1}
+ lo.style = osc_styles.TransBg
+ lo.layer = 10
+ lo.alpha[3] = 0
+
+ --
+ -- Alignment
+ --
+ local refX = osc_w / 2
+ local refY = posY
+ local geo
+
+ --
+ -- Seekbar
+ --
+ new_element('bgbar1', 'box')
+ lo = add_layout('bgbar1')
+ lo.geometry = {x = refX , y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 2}
+ lo.layer = 13
+ lo.style = osc_styles.SeekbarBg
+ lo.alpha[1] = 128
+ lo.alpha[3] = 128
+
+ lo = add_layout('seekbar')
+ lo.geometry = {x = refX, y = refY - 96 , an = 5, w = osc_geo.w - 50, h = 16}
+ lo.style = osc_styles.SeekbarFg
+ lo.slider.gap = 7
+ lo.slider.tooltip_style = osc_styles.Tooltip
+ lo.slider.tooltip_an = 2
+
+ -- buttons
+ lo = add_layout('pl_prev')
+ lo.geometry = {x = refX - 120, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl2
+
+ lo = add_layout('skipback')
+ lo.geometry = {x = refX - 60, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl2
+
+
+ lo = add_layout('playpause')
+ lo.geometry = {x = refX, y = refY - 40 , an = 5, w = 45, h = 45}
+ lo.style = osc_styles.Ctrl1
+
+ lo = add_layout('skipfrwd')
+ lo.geometry = {x = refX + 60, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl2
+
+ lo = add_layout('pl_next')
+ lo.geometry = {x = refX + 120, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl2
+
+
+ -- Time
+ lo = add_layout('tc_left')
+ lo.geometry = {x = 25, y = refY - 84, an = 7, w = 64, h = 20}
+ lo.style = osc_styles.Time
+
+
+ lo = add_layout('tc_right')
+ lo.geometry = {x = osc_geo.w - 25 , y = refY -84, an = 9, w = 64, h = 20}
+ lo.style = osc_styles.Time
+
+ lo = add_layout('cy_audio')
+ lo.geometry = {x = 37, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+
+ lo = add_layout('cy_sub')
+ lo.geometry = {x = 87, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+
+ lo = add_layout('tog_fs')
+ lo.geometry = {x = osc_geo.w - 37, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+
+ lo = add_layout('tog_info')
+ lo.geometry = {x = osc_geo.w - 87, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+
+ geo = { x = 25, y = refY - 132, an = 1, w = osc_geo.w - 50, h = 48 }
+ lo = add_layout('title')
+ lo.geometry = geo
+ lo.style = string.format('%s{\\clip(%f,%f,%f,%f)}', osc_styles.Title,
+ geo.x, geo.y - geo.h, geo.x + geo.w , geo.y)
+ lo.alpha[3] = 0
+end
+
+-- Validate string type user options
+function validate_user_opts()
+ if user_opts.windowcontrols ~= 'auto' and
+ user_opts.windowcontrols ~= 'yes' and
+ user_opts.windowcontrols ~= 'no' then
+ msg.warn('windowcontrols cannot be \'' ..
+ user_opts.windowcontrols .. '\'. Ignoring.')
+ user_opts.windowcontrols = 'auto'
+ end
+end
+
+function update_options(list)
+ validate_user_opts()
+ request_tick()
+ visibility_mode(user_opts.visibility, true)
+ request_init()
+end
+
+-- OSC INIT
+function osc_init()
+ msg.debug('osc_init')
+
+ -- set canvas resolution according to display aspect and scaling setting
+ local baseResY = 720
+ local display_w, display_h, display_aspect = mp.get_osd_size()
+ local scale = 1
+
+ if (mp.get_property('video') == 'no') then -- dummy/forced window
+ scale = user_opts.scaleforcedwindow
+ elseif state.fullscreen then
+ scale = user_opts.scalefullscreen
+ else
+ scale = user_opts.scalewindowed
+ end
+
+ if user_opts.vidscale 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 ne
+
+ -- playlist buttons
+ -- prev
+ ne = new_element('pl_prev', 'button')
+
+ ne.content = '\xEF\x8E\xB5'
+ ne.enabled = (pl_pos > 1) or (loop ~= 'no')
+ ne.eventresponder['mbtn_left_up'] =
+ function ()
+ mp.commandv('playlist-prev', 'weak')
+ end
+ ne.eventresponder['mbtn_right_up'] =
+ function () show_message(get_playlist()) end
+
+ --next
+ ne = new_element('pl_next', 'button')
+
+ ne.content = '\xEF\x8E\xB4'
+ ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= 'no')
+ ne.eventresponder['mbtn_left_up'] =
+ function ()
+ mp.commandv('playlist-next', 'weak')
+ end
+ ne.eventresponder['mbtn_right_up'] =
+ function () show_message(get_playlist()) end
+
+
+ --play control buttons
+ --playpause
+ ne = new_element('playpause', 'button')
+
+ ne.content = function ()
+ if mp.get_property('pause') == 'yes' then
+ return ('\xEF\x8E\xA7')
+ else
+ return ('\xEF\x8E\xAA')
+ end
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('cycle', 'pause') end
+ --ne.eventresponder['mbtn_right_up'] =
+ -- function () mp.commandv('script-binding', 'open-file-dialog') end
+
+ --skipback
+ ne = new_element('skipback', 'button')
+
+ ne.softrepeat = true
+ ne.content = '\xEF\x8E\xA0'
+ ne.eventresponder['mbtn_left_down'] =
+ --function () mp.command('seek -5') end
+ function () mp.commandv('seek', -5, 'relative', 'keyframes') end
+ ne.eventresponder['shift+mbtn_left_down'] =
+ function () mp.commandv('frame-back-step') end
+ ne.eventresponder['mbtn_right_down'] =
+ --function () mp.command('seek -60') end
+ function () mp.commandv('seek', -60, 'relative', 'keyframes') end
+
+ --skipfrwd
+ ne = new_element('skipfrwd', 'button')
+
+ ne.softrepeat = true
+ ne.content = '\xEF\x8E\x9F'
+ ne.eventresponder['mbtn_left_down'] =
+ --function () mp.command('seek +5') end
+ function () mp.commandv('seek', 5, 'relative', 'keyframes') end
+ ne.eventresponder['shift+mbtn_left_down'] =
+ function () mp.commandv('frame-step') end
+ ne.eventresponder['mbtn_right_down'] =
+ --function () mp.command('seek +60') end
+ function () mp.commandv('seek', 60, 'relative', 'keyframes') end
+
+ --
+ update_tracklist()
+
+ --cy_audio
+ ne = new_element('cy_audio', 'button')
+ ne.enabled = (#tracks_osc.audio > 0)
+ ne.visible = (osc_param.playresx >= 540)
+ ne.content = '\xEF\x8E\xB7'
+ ne.tooltip_style = osc_styles.Tooltip
+ ne.tooltipF = function ()
+ local msg = texts.off
+ if not (get_track('audio') == 0) then
+ msg = (texts.audio .. ' [' .. get_track('audio') .. ' ∕ ' .. #tracks_osc.audio .. '] ')
+ local prop = mp.get_property('current-tracks/audio/lang')
+ if not prop then
+ prop = texts.na
+ end
+ msg = msg .. '[' .. prop .. ']'
+ prop = mp.get_property('current-tracks/audio/title')
+ if prop then
+ msg = msg .. ' ' .. prop
+ end
+ return msg
+ end
+ return msg
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function () set_track('audio', 1) end
+ ne.eventresponder['mbtn_right_up'] =
+ function () set_track('audio', -1) end
+ ne.eventresponder['mbtn_mid_up'] =
+ function () show_message(get_tracklist('audio')) end
+
+ --cy_sub
+ ne = new_element('cy_sub', 'button')
+ ne.enabled = (#tracks_osc.sub > 0)
+ ne.visible = (osc_param.playresx >= 600)
+ ne.content = '\xEF\x8F\x93'
+ ne.tooltip_style = osc_styles.Tooltip
+ ne.tooltipF = function ()
+ local msg = texts.off
+ if not (get_track('sub') == 0) then
+ msg = (texts.subtitle .. ' [' .. get_track('sub') .. ' ∕ ' .. #tracks_osc.sub .. '] ')
+ local prop = mp.get_property('current-tracks/sub/lang')
+ if not prop then
+ prop = texts.na
+ end
+ msg = msg .. '[' .. prop .. ']'
+ prop = mp.get_property('current-tracks/sub/title')
+ if prop then
+ msg = msg .. ' ' .. prop
+ end
+ return msg
+ end
+ return msg
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function () set_track('sub', 1) end
+ ne.eventresponder['mbtn_right_up'] =
+ function () set_track('sub', -1) end
+ ne.eventresponder['mbtn_mid_up'] =
+ function () show_message(get_tracklist('sub')) end
+
+ --tog_fs
+ ne = new_element('tog_fs', 'button')
+ ne.content = function ()
+ if (state.fullscreen) then
+ return ('\xEF\x85\xAC')
+ else
+ return ('\xEF\x85\xAD')
+ end
+ end
+ ne.visible = (osc_param.playresx >= 540)
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('cycle', 'fullscreen') end
+
+ --tog_info
+ ne = new_element('tog_info', 'button')
+ ne.content = ''
+ ne.visible = (osc_param.playresx >= 600)
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('script-binding', 'stats/display-stats-toggle') end
+
+ -- title
+ ne = new_element('title', 'button')
+ ne.content = function ()
+ local title = mp.command_native({'expand-text', user_opts.title})
+ if state.paused then
+ title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{')
+ else
+ title = ' '
+ end
+ return not (title == '') and title or ' '
+ end
+ ne.visible = osc_param.playresy >= 320 and user_opts.showtitle
+
+ --seekbar
+ ne = new_element('seekbar', 'slider')
+
+ ne.enabled = not (mp.get_property('percent-pos') == nil)
+ ne.slider.markerF = function ()
+ local duration = mp.get_property_number('duration', nil)
+ if not (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 () return mp.get_property_number('percent-pos', nil) end
+ ne.slider.tooltipF = function (pos)
+ local duration = mp.get_property_number('duration', nil)
+ if not ((duration == nil) or (pos == nil)) then
+ possec = duration * (pos / 100)
+ return mp.format_time(possec)
+ else
+ return ''
+ end
+ end
+ ne.slider.seekRangesF = function()
+ 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', nil)
+ 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
+ ne.eventresponder['mouse_move'] = --keyframe seeking when mouse is dragged
+ 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
+ local seekto = get_slider_value(element)
+ if (element.state.lastseek == nil) or
+ (not (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'] = --exact seeks on single clicks
+ function (element)
+ mp.commandv('seek', get_slider_value(element), 'absolute-percent', 'exact')
+ element.state.mbtnleft = true
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function (element) element.state.mbtnleft = false end
+ ne.eventresponder['mbtn_right_down'] = --seeks to chapter start
+ function (element)
+ local duration = mp.get_property_number('duration', nil)
+ if not (duration == nil) then
+ local chapters = mp.get_property_native('chapter-list', {})
+ if #chapters > 0 then
+ local pos = get_slider_value(element)
+ local ch = #chapters
+ for n = 1, ch do
+ if chapters[n].time / duration * 100 >= pos then
+ ch = n - 1
+ break
+ end
+ end
+ mp.commandv('set', 'chapter', ch - 1)
+ --if chapters[ch].title then show_message(chapters[ch].time) end
+ end
+ end
+ end
+ ne.eventresponder['reset'] =
+ function (element) element.state.lastseek = nil end
+
+
+ -- tc_left (current pos)
+ ne = new_element('tc_left', 'button')
+ ne.content = function () return (mp.get_property_osd('playback-time')) end
+
+ -- tc_right (total/remaining time)
+ ne = new_element('tc_right', 'button')
+ ne.content = function ()
+ if (mp.get_property_number('duration', 0) <= 0) then return '--:--:--' end
+ if (state.rightTC_trem) then
+ return ('-'..mp.get_property_osd('playtime-remaining'))
+ else
+ return (mp.get_property_osd('duration'))
+ end
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function () state.rightTC_trem = not state.rightTC_trem end
+
+ -- load layout
+ layouts()
+
+ -- load window controls
+ if window_controls_enabled() then
+ window_controls()
+ end
+
+ --do something with the elements
+ prepare_elements()
+end
+
+function shutdown()
+
+end
+
+--
+-- Other important stuff
+--
+
+
+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()
+
+ osc_visible(true)
+
+ if (user_opts.fadeduration > 0) then
+ state.anitype = nil
+ end
+end
+
+function hide_osc()
+ msg.trace('hide_osc')
+ 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
+ render_wipe()
+ elseif (user_opts.fadeduration > 0) then
+ if not(state.osc_visible == false) then
+ state.anitype = 'out'
+ request_tick()
+ end
+ else
+ osc_visible(false)
+ end
+end
+
+function osc_visible(visible)
+ if state.osc_visible ~= visible then
+ state.osc_visible = visible
+ end
+ request_tick()
+end
+
+function pause_state(name, enabled)
+ state.paused = enabled
+ if user_opts.showtitle then
+ if enabled then
+ state.lastvisibility = user_opts.visibility
+ visibility_mode("always", true)
+ show_osc()
+ else
+ visibility_mode(state.lastvisibility, true)
+ end
+ end
+ request_tick()
+end
+
+function cache_state(name, st)
+ state.cache_state = st
+ request_tick()
+end
+
+-- 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.
+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
+
+function mouse_leave()
+ if get_hidetimeout() >= 0 then
+ hide_osc()
+ end
+ -- reset mouse position
+ state.last_mouseX, state.last_mouseY = nil, nil
+ state.mouse_in_window = false
+end
+
+function request_init()
+ state.initREQ = true
+ request_tick()
+end
+
+-- Like request_init(), but also request an immediate update
+function request_init_resize()
+ request_init()
+ -- ensure immediate update
+ state.tick_timer:kill()
+ state.tick_timer.timeout = 0
+ state.tick_timer:resume()
+end
+
+function render_wipe()
+ msg.trace('render_wipe()')
+ state.osd:remove()
+end
+
+function render()
+ msg.trace('rendering')
+ local current_screen_sizeX, current_screen_sizeY, aspect = 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 not (state.mp_screen_sizeX == current_screen_sizeX
+ and state.mp_screen_sizeY == current_screen_sizeY) then
+
+ request_init_resize()
+
+ state.mp_screen_sizeX = current_screen_sizeX
+ state.mp_screen_sizeY = current_screen_sizeY
+ end
+
+ -- init management
+ if 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 not(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
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+ end
+ else
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+ end
+
+ --mouse show/hide area
+ for k,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 k,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 (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
+ mouse_over_osc = true
+ end
+ end
+ end
+
+ -- autohide
+ if not (state.showtime == nil) and (get_hidetimeout() >= 0) then
+ local timeout = state.showtime + (get_hidetimeout()/1000) - now
+ if timeout <= 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()
+
+ -- Messages
+ render_message(ass)
+
+ -- actual OSC
+ if state.osc_visible then
+ render_elements(ass)
+ end
+
+ -- submit
+ set_osd(osc_param.playresy * osc_param.display_aspect,
+ osc_param.playresy, ass.text)
+end
+
+--
+-- Eventhandling
+--
+
+local function element_has_action(element, action)
+ return element and element.eventresponder and
+ element.eventresponder[action]
+end
+
+function process_event(source, what)
+ local action = string.format('%s%s', source,
+ what and ('_' .. what) or '')
+
+ if what == 'down' or what == 'press' then
+
+ 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
+ (not ((state.last_mouseX == nil) or (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
+ show_osc()
+ 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
+ request_tick()
+ end
+end
+
+function show_logo()
+ local osd_w, osd_h = 640, 360
+ local logo_x, logo_y = osd_w/2, osd_h/2-20
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ ass:pos(logo_x, logo_y)
+ ass:append('{\\1c&H8E348D&\\3c&H0&\\3a&H60&\\blur1\\bord0.5}')
+ ass:draw_start()
+ ass_draw_cir_cw(ass, 0, 0, 100)
+ ass:draw_stop()
+
+ ass:new_event()
+ ass:pos(logo_x, logo_y)
+ ass:append('{\\1c&H632462&\\bord0}')
+ ass:draw_start()
+ ass_draw_cir_cw(ass, 6, -6, 75)
+ ass:draw_stop()
+
+ ass:new_event()
+ ass:pos(logo_x, logo_y)
+ ass:append('{\\1c&HFFFFFF&\\bord0}')
+ ass:draw_start()
+ ass_draw_cir_cw(ass, -4, 4, 50)
+ ass:draw_stop()
+
+ ass:new_event()
+ ass:pos(logo_x, logo_y)
+ ass:append('{\\1c&H632462&\\bord&}')
+ ass:draw_start()
+ ass:move_to(-20, -20)
+ ass:line_to(23.3, 5)
+ ass:line_to(-20, 30)
+ ass:draw_stop()
+
+ ass:new_event()
+ ass:pos(logo_x, logo_y+110)
+ ass:an(8)
+ ass:append(texts.welcome)
+ set_osd(osd_w, osd_h, ass.text)
+end
+
+-- called by mpv on every frame
+function tick()
+ if (not state.enabled) then return end
+
+ if (state.idle) then
+ show_logo()
+ -- render idle message
+ msg.trace('idle message')
+
+ 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
+ set_osd(osc_param.playresy, osc_param.playresy, '')
+ end
+
+ state.tick_last_time = mp.get_time()
+
+ if state.anitype ~= nil then
+ request_tick()
+ end
+end
+
+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
+
+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
+
+validate_user_opts()
+
+mp.register_event('shutdown', shutdown)
+mp.register_event('start-file', request_init)
+mp.observe_property('track-list', nil, request_init)
+mp.observe_property('playlist', nil, request_init)
+
+mp.register_script_message('osc-message', show_message)
+mp.register_script_message('osc-chapterlist', function(dur)
+ show_message(get_chapterlist(), dur)
+end)
+mp.register_script_message('osc-playlist', function(dur)
+ show_message(get_playlist(), dur)
+end)
+mp.register_script_message('osc-tracklist', function(dur)
+ local msg = {}
+ for k,v in pairs(nicetypes) do
+ table.insert(msg, get_tracklist(k))
+ end
+ show_message(table.concat(msg, '\n\n'), dur)
+end)
+
+mp.observe_property('fullscreen', 'bool',
+ function(name, val)
+ state.fullscreen = val
+ request_init_resize()
+ end
+)
+mp.observe_property('border', 'bool',
+ function(name, val)
+ state.border = val
+ request_init_resize()
+ end
+)
+mp.observe_property('window-maximized', 'bool',
+ function(name, val)
+ state.maximized = val
+ request_init_resize()
+ end
+)
+mp.observe_property('idle-active', 'bool',
+ function(name, val)
+ state.idle = val
+ request_tick()
+ end
+)
+mp.observe_property('pause', 'bool', pause_state)
+mp.observe_property('demuxer-cache-state', 'native', cache_state)
+mp.observe_property('vo-configured', 'bool', function(name, val)
+ request_tick()
+end)
+mp.observe_property('playback-time', 'number', function(name, val)
+ request_tick()
+end)
+mp.observe_property('osd-dimensions', 'native', function(name, val)
+ -- (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()
+end)
+
+-- mouse show/hide bindings
+mp.set_key_bindings({
+ {'mouse_move', function(e) process_event('mouse_move', nil) end},
+ {'mouse_leave', mouse_leave},
+}, 'showhide', 'force')
+mp.set_key_bindings({
+ {'mouse_move', function(e) 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(e) process_event('mbtn_left', 'up') end,
+ function(e) process_event('mbtn_left', 'down') end},
+ {'mbtn_right', function(e) process_event('mbtn_right', 'up') end,
+ function(e) process_event('mbtn_right', 'down') end},
+ {'mbtn_mid', function(e) process_event('mbtn_mid', 'up') end,
+ function(e) process_event('mbtn_mid', 'down') end},
+ {'wheel_up', function(e) process_event('wheel_up', 'press') end},
+ {'wheel_down', function(e) process_event('wheel_down', 'press') end},
+ {'mbtn_left_dbl', 'ignore'},
+ {'mbtn_right_dbl', 'ignore'},
+}, 'input', 'force')
+mp.enable_key_bindings('input')
+
+mp.set_key_bindings({
+ {'mbtn_left', function(e) process_event('mbtn_left', 'up') end,
+ function(e) process_event('mbtn_left', 'down') end},
+}, 'window-controls', 'force')
+mp.enable_key_bindings('window-controls')
+
+function get_hidetimeout()
+ if user_opts.visibility == 'always' then
+ return -1 -- disable autohide
+ end
+ return user_opts.hidetimeout
+end
+
+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.
+function visibility_mode(mode, no_osd)
+ 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
+
+ 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
+ -- recalcuated 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
+ request_tick()
+end
+
+visibility_mode(user_opts.visibility, true)
+mp.register_script_message('osc-visibility', visibility_mode)
+mp.add_key_binding(nil, 'visibility', function() visibility_mode('cycle') end)
+
+set_virt_mouse_area(0, 0, 0, 0, 'input')
+set_virt_mouse_area(0, 0, 0, 0, 'window-controls')
diff --git a/mpv/scripts/uosc.lua b/mpv/scripts/uosc.lua
deleted file mode 100644
index a2a5074..0000000
--- a/mpv/scripts/uosc.lua
+++ /dev/null
@@ -1,3230 +0,0 @@
---[[
-
-uosc 2.10.1 - 2020-Jun-20 | https://github.com/darsain/uosc
-
-Minimalist cursor proximity based UI for MPV player.
-
-uosc replaces the default osc UI, so that has to be disabled first.
-Place these options into your `mpv.conf` file:
-
-```
-# required so that the 2 UIs don't fight each other
-osc=no
-# uosc provides its own seeking/volume indicators, so you also don't need this
-osd-bar=no
-# uosc will draw its own window controls if you disable window border
-border=no
-```
-
-Options go in `script-opts/uosc.conf`. Defaults:
-
-```
-# timeline size when fully retracted, 0 will hide it completely
-timeline_size_min=2
-# timeline size when fully expanded, in pixels, 0 to disable
-timeline_size_max=40
-# same as ^ but when in fullscreen
-timeline_size_min_fullscreen=0
-timeline_size_max_fullscreen=60
-# same thing as calling toggle-progress command once on startup
-timeline_start_hidden=no
-# timeline opacity
-timeline_opacity=0.8
-# top (and bottom in no-border mode) border of background color to help visually
-# separate elapsed bar from a video of similar color or desktop background
-timeline_border=1
-# when scrolling above timeline, wheel will seek by this amount of seconds
-timeline_step=5
-# display seekable buffered ranges for streaming videos, syntax `color:opacity`,
-# color is an BBGGRR hex code, set to `none` to disable
-timeline_cached_ranges=345433:0.5
-# floating number font scale adjustment
-timeline_font_scale=1
-
-# timeline chapters style: none, dots, lines, lines-top, lines-bottom
-chapters=dots
-chapters_opacity=0.3
-
-# where to display volume controls: none, left, right
-volume=right
-volume_size=40
-volume_size_fullscreen=60
-volume_opacity=0.8
-volume_border=1
-volume_step=1
-volume_font_scale=1
-
-# playback speed widget: mouse drag or wheel to change, click to reset
-speed=no
-speed_size=46
-speed_size_fullscreen=68
-speed_opacity=1
-speed_step=0.1
-speed_font_scale=1
-
-# controls all menus, such as context menu, subtitle loader/selector, etc
-menu_item_height=36
-menu_item_height_fullscreen=50
-menu_wasd_navigation=no
-menu_hjkl_navigation=no
-menu_opacity=0.8
-menu_font_scale=1
-
-# top bar with window controls and media title shown only in no-border mode
-top_bar_size=40
-top_bar_size_fullscreen=46
-top_bar_controls=yes
-top_bar_title=yes
-
-# pause video on clicks shorter than this number of milliseconds, 0 to disable
-pause_on_click_shorter_than=0
-# flash duration in milliseconds used by `flash-{element}` commands
-flash_duration=400
-# distances in pixels below which elements are fully faded in/out
-proximity_in=40
-proximity_out=120
-# BBGGRR - BLUE GREEN RED hex color codes
-color_foreground=ffffff
-color_foreground_text=000000
-color_background=000000
-color_background_text=ffffff
-# use bold font weight throughout the whole UI
-font_bold=no
-# hide UI when mpv autohides the cursor
-autohide=no
-# can be: none, flash, static
-pause_indicator=flash
-# load first file when calling next on a last file in a directory and vice versa
-directory_navigation_loops=no
-# file types to look for when navigating media files
-media_types=3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv
-# file types to look for when loading external subtitles
-subtitle_types=aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt
-# used to approximate text width
-# if you are using some wide font and see a lot of right side clipping in menus,
-# try bumping this up
-font_height_to_letter_width_ratio=0.5
-
-# `chapter_ranges` lets you transform chapter indicators into range indicators.
-#
-# Chapter range definition syntax:
-# ```
-# start_pattern<color:opacity>end_pattern
-# ```
-#
-# Multiple start and end patterns can be defined by separating them with `|`:
-# ```
-# p1|pN<color:opacity>p1|pN
-# ```
-#
-# Multiple chapter ranges can be defined by separating them with comma:
-#
-# chapter_ranges=range1,rangeN
-#
-# One of `start_pattern`s can be a custom keyword `{bof}` that will match
-# beginning of file when it makes sense.
-#
-# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of
-# file when it makes sense.
-#
-# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial).
-# They only need to occur in a title, not match it completely.
-# Matching is case insensitive.
-#
-# `color` is a `bbggrr` hexadecimal color code.
-# `opacity` is a float number from 0 to 1.
-#
-# Examples:
-#
-# Display anime openings and endings as ranges:
-# ```
-# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
-# ```
-#
-# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
-# ```
-# chapter_ranges=sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
-# ```
-chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
-```
-
-Available keybindings (place into `input.conf`):
-
-```
-Key script-binding uosc/peek-timeline
-Key script-binding uosc/toggle-progress
-Key script-binding uosc/flash-timeline
-Key script-binding uosc/flash-volume
-Key script-binding uosc/flash-speed
-Key script-binding uosc/menu
-Key script-binding uosc/load-subtitles
-Key script-binding uosc/subtitles
-Key script-binding uosc/audio
-Key script-binding uosc/video
-Key script-binding uosc/playlist
-Key script-binding uosc/chapters
-Key script-binding uosc/open-file
-Key script-binding uosc/next
-Key script-binding uosc/prev
-Key script-binding uosc/first
-Key script-binding uosc/last
-Key script-binding uosc/next-file
-Key script-binding uosc/prev-file
-Key script-binding uosc/first-file
-Key script-binding uosc/last-file
-Key script-binding uosc/delete-file-next
-Key script-binding uosc/delete-file-quit
-Key script-binding uosc/show-in-directory
-Key script-binding uosc/open-config-directory
-```
-]]
-
-if mp.get_property('osc') == 'yes' then
- mp.msg.info('Disabled because original osc is enabled!')
- return
-end
-
-local assdraw = require('mp.assdraw')
-local opt = require('mp.options')
-local utils = require('mp.utils')
-local msg = require('mp.msg')
-local osd = mp.create_osd_overlay('ass-events')
-local infinity = 1e309
-
--- OPTIONS/CONFIG/STATE
-local options = {
- timeline_size_min = 2,
- timeline_size_max = 40,
- timeline_size_min_fullscreen = 0,
- timeline_size_max_fullscreen = 60,
- timeline_start_hidden = false,
- timeline_opacity = 0.8,
- timeline_border = 1,
- timeline_step = 5,
- timeline_cached_ranges = '345433:0.5',
- timeline_font_scale = 1,
-
- chapters = 'dots',
- chapters_opacity = 0.3,
-
- volume = 'right',
- volume_size = 40,
- volume_size_fullscreen = 60,
- volume_opacity = 0.8,
- volume_border = 1,
- volume_step = 1,
- volume_font_scale = 1,
-
- speed = false,
- speed_size = 46,
- speed_size_fullscreen = 68,
- speed_opacity = 1,
- speed_step = 0.1,
- speed_font_scale = 1,
-
- menu_item_height = 36,
- menu_item_height_fullscreen = 50,
- menu_wasd_navigation = false,
- menu_hjkl_navigation = false,
- menu_opacity = 0.8,
- menu_font_scale = 1,
-
- top_bar_size = 40,
- top_bar_size_fullscreen = 46,
- top_bar_controls = true,
- top_bar_title = true,
-
- pause_on_click_shorter_than = 0,
- flash_duration = 400,
- proximity_in = 40,
- proximity_out = 120,
- color_foreground = 'ffffff',
- color_foreground_text = '000000',
- color_background = '000000',
- color_background_text = 'ffffff',
- font_bold = false,
- autohide = false,
- pause_indicator = 'flash',
- directory_navigation_loops = false,
- media_types = '3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv',
- subtitle_types = 'aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt',
- font_height_to_letter_width_ratio = 0.5,
- chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end',
-}
-opt.read_options(options, 'uosc')
-local config = {
- render_delay = 0.03, -- sets max rendering frequency
- font = mp.get_property('options/osd-font'),
- menu_parent_opacity = 0.4,
- menu_min_width = 260
-}
-local bold_tag = options.font_bold and '\\b1' or ''
-local display = {
- width = 1280,
- height = 720,
- aspect = 1.77778,
-}
-local cursor = {
- hidden = true, -- true when autohidden or outside of the player window
- x = 0,
- y = 0,
-}
-local state = {
- os = (function()
- if os.getenv('windir') ~= nil then return 'windows' end
- local homedir = os.getenv('HOME')
- if homedir ~= nil and string.sub(homedir,1,6) == '/Users' then return 'macos' end
- return 'linux'
- end)(),
- cwd = mp.get_property('working-directory'),
- media_title = '',
- duration = nil,
- position = nil,
- pause = false,
- chapters = nil,
- chapter_ranges = nil,
- fullscreen = mp.get_property_native('fullscreen'),
- maximized = mp.get_property_native('window-maximized'),
- render_timer = nil,
- render_last_time = 0,
- volume = nil,
- volume_max = nil,
- mute = nil,
- cursor_autohide_timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function()
- if not options.autohide then return end
- handle_mouse_leave()
- end),
- mouse_bindings_enabled = false,
- cached_ranges = nil,
-}
-local forced_key_bindings -- defined at the bottom next to events
-
--- HELPERS
-
-function round(number)
- local modulus = number % 1
- return modulus < 0.5 and math.floor(number) or math.ceil(number)
-end
-
-function call_me_maybe(fn, value1, value2, value3)
- if fn then fn(value1, value2, value3) end
-end
-
-function split(str, pattern)
- local list = {}
- local full_pattern = '(.-)' .. pattern
- local last_end = 1
- local start_index, end_index, capture = str:find(full_pattern, 1)
- while start_index do
- list[#list +1] = capture
- last_end = end_index + 1
- start_index, end_index, capture = str:find(full_pattern, last_end)
- end
- if last_end <= (#str + 1) then
- capture = str:sub(last_end)
- list[#list +1] = capture
- end
- return list
-end
-
-function itable_find(haystack, needle)
- local is_needle = type(needle) == 'function' and needle or function(index, value)
- return value == needle
- end
- for index, value in ipairs(haystack) do
- if is_needle(index, value) then return index, value end
- end
-end
-
-function itable_filter(haystack, needle)
- local is_needle = type(needle) == 'function' and needle or function(index, value)
- return value == needle
- end
- local filtered = {}
- for index, value in ipairs(haystack) do
- if is_needle(index, value) then filtered[#filtered + 1] = value end
- end
- return filtered
-end
-
-function itable_remove(haystack, needle)
- local should_remove = type(needle) == 'function' and needle or function(value)
- return value == needle
- end
- local new_table = {}
- for _, value in ipairs(haystack) do
- if not should_remove(value) then
- new_table[#new_table + 1] = value
- end
- end
- return new_table
-end
-
-function itable_slice(haystack, start_pos, end_pos)
- start_pos = start_pos and start_pos or 1
- end_pos = end_pos and end_pos or #haystack
-
- if end_pos < 0 then end_pos = #haystack + end_pos + 1 end
- if start_pos < 0 then start_pos = #haystack + start_pos + 1 end
-
- local new_table = {}
- for index, value in ipairs(haystack) do
- if index >= start_pos and index <= end_pos then
- new_table[#new_table + 1] = value
- end
- end
- return new_table
-end
-
-function table_copy(table)
- local new_table = {}
- for key, value in pairs(table) do new_table[key] = value end
- return new_table
-end
-
--- Sorting comparator close to (but not exactly) how file explorers sort files
-local word_order_comparator = (function()
- local symbol_order
- local default_order
-
- if state.os == 'win' then
- symbol_order = {
- ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
- ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
- ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
- }
- default_order = 21
- else
- symbol_order = {
- ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
- ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
- ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
- }
- default_order = 21
- end
-
- return function (a, b)
- a = a:lower()
- b = b:lower()
- for i = 1, math.max(#a, #b) do
- local ai = a:sub(i, i)
- local bi = b:sub(i, i)
- if ai == nil and bi then return true end
- if bi == nil and ai then return false end
- local a_order = symbol_order[ai] or default_order
- local b_order = symbol_order[bi] or default_order
- if a_order == b_order then
- return a < b
- else
- return a_order < b_order
- end
- end
- end
-end)()
-
--- Creates in-between frames to animate value from `from` to `to` numbers.
--- Returns function that terminates animation.
--- `to` can be a function that returns target value, useful for movable targets.
--- `speed` is an optional float between 1-instant and 0-infinite duration
--- `callback` is called either on animation end, or when animation is canceled
-function tween(from, to, setter, speed, callback)
- if type(speed) ~= 'number' then
- callback = speed
- speed = 0.3
- end
- local timeout
- local getTo = type(to) == 'function' and to or function() return to end
- local cutoff = math.abs(getTo() - from) * 0.01
- function tick()
- from = from + ((getTo() - from) * speed)
- local is_end = math.abs(getTo() - from) <= cutoff
- setter(is_end and getTo() or from)
- request_render()
- if is_end then
- call_me_maybe(callback)
- else
- timeout:resume()
- end
- end
- timeout = mp.add_timeout(0.016, tick)
- tick()
- return function()
- timeout:kill()
- call_me_maybe(callback)
- end
-end
-
--- Kills ongoing animation if one is already running on this element.
--- Killed animation will not get its `on_end` called.
-function tween_element(element, from, to, setter, speed, callback)
- if type(speed) ~= 'number' then
- callback = speed
- speed = 0.3
- end
-
- tween_element_stop(element)
-
- element.stop_current_animation = tween(
- from, to,
- function(value) setter(element, value) end,
- speed,
- function()
- element.stop_current_animation = nil
- call_me_maybe(callback, element)
- end
- )
-end
-
--- Stopped animation will not get its on_end called.
-function tween_element_is_tweening(element)
- return element and element.stop_current_animation
-end
-
--- Stopped animation will not get its on_end called.
-function tween_element_stop(element)
- call_me_maybe(element and element.stop_current_animation)
-end
-
--- Helper to automatically use an element property setter
-function tween_element_property(element, prop, from, to, speed, callback)
- tween_element(element, from, to, function(_, value) element[prop] = value end, speed, callback)
-end
-
-function get_point_to_rectangle_proximity(point, rect)
- local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx + 1)
- local dy = math.max(rect.ay - point.y, 0, point.y - rect.by + 1)
- return math.sqrt(dx*dx + dy*dy);
-end
-
-function text_width_estimate(letters, font_size)
- return letters and letters * font_size * options.font_height_to_letter_width_ratio or 0
-end
-
-function opacity_to_alpha(opacity)
- return 255 - math.ceil(255 * opacity)
-end
-
-function ass_opacity(opacity, fraction)
- fraction = fraction ~= nil and fraction or 1
- if type(opacity) == 'number' then
- return string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
- else
- return string.format(
- '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
- opacity_to_alpha((opacity[1] or 0) * fraction),
- opacity_to_alpha((opacity[2] or 0) * fraction),
- opacity_to_alpha((opacity[3] or 0) * fraction),
- opacity_to_alpha((opacity[4] or 0) * fraction)
- )
- end
-end
-
--- Ensures path is absolute and normalizes slashes to the current platform
-function normalize_path(path)
- if not path or is_protocol(path) then return path end
-
- -- Ensure path is absolute
- if not (path:match('^/') or path:match('^%a+:') or path:match('^\\\\')) then
- path = utils.join_path(state.cwd, path)
- end
-
- -- Use proper slashes
- if state.os == 'windows' then
- return path:gsub('/', '\\')
- else
- return path:gsub('\\', '/')
- end
-end
-
--- Check if path is a protocol, such as `http://...`
-function is_protocol(path)
- return path:match('^%a[%a%d-_]+://')
-end
-
-function get_extension(path)
- local parts = split(path, '%.')
- return parts and #parts > 1 and parts[#parts] or nil
-end
-
--- Serializes path into its semantic parts
-function serialize_path(path)
- if not path or is_protocol(path) then return end
- path = normalize_path(path)
- local parts = split(path, '[\\/]+')
- if parts[#parts] == '' then table.remove(parts, #parts) end -- remove trailing separator
- local basename = parts and parts[#parts] or path
- local dirname = #parts > 1 and table.concat(itable_slice(parts, 1, #parts - 1), state.os == 'windows' and '\\' or '/') or nil
- local dot_split = split(basename, '%.')
- return {
- path = path:sub(-1) == ':' and state.os == 'windows' and path..'\\' or path,
- is_root = dirname == nil,
- dirname = dirname,
- basename = basename,
- filename = #dot_split > 1 and table.concat(itable_slice(dot_split, 1, #dot_split - 1), '.') or basename,
- extension = #dot_split > 1 and dot_split[#dot_split] or nil,
- }
-end
-
-function get_files_in_directory(directory, allowed_types)
- local files, error = utils.readdir(directory, 'files')
-
- if not files then
- msg.error('Retrieving files failed: '..(error or ''))
- return
- end
-
- -- Filter only requested file types
- if allowed_types then
- files = itable_filter(files, function(_, file)
- local extension = get_extension(file)
- return extension and itable_find(allowed_types, extension:lower())
- end)
- end
-
- table.sort(files, word_order_comparator)
-
- return files
-end
-
-function get_adjacent_file(file_path, direction, allowed_types)
- local current_file = serialize_path(file_path)
- local files = get_files_in_directory(current_file.dirname, allowed_types)
-
- if not files then return end
-
- for index, file in ipairs(files) do
- if current_file.basename == file then
- if direction == 'forward' then
- if files[index + 1] then return utils.join_path(current_file.dirname, files[index + 1]) end
- if options.directory_navigation_loops and files[1] then return utils.join_path(current_file.dirname, files[1]) end
- else
- if files[index - 1] then return utils.join_path(current_file.dirname, files[index - 1]) end
- if options.directory_navigation_loops and files[#files] then return utils.join_path(current_file.dirname, files[#files]) end
- end
-
- -- This is the only file in directory
- return nil
- end
- end
-end
-
--- Ensures chapters are in chronological order
-function get_normalized_chapters()
- local chapters = mp.get_property_native('chapter-list')
-
- if not chapters then return end
-
- -- Copy table
- chapters = itable_slice(chapters)
-
- -- Ensure chronological order of chapters
- table.sort(chapters, function(a, b) return a.time < b.time end)
-
- return chapters
-end
-
--- Element
---[[
-Signature:
-{
- -- enables capturing button groups for this element
- captures = {mouse_buttons = true, wheel = true},
- -- element rectangle coordinates
- ax = 0, ay = 0, bx = 0, by = 0,
- -- cursor<>element relative proximity as a 0-1 floating number
- -- where 0 = completely away, and 1 = touching/hovering
- -- so it's easy to work with and throw into equations
- proximity = 0,
- -- raw cursor<>element proximity in pixels
- proximity_raw = infinity,
- -- called when element is created
- ?init = function(this),
- -- called manually when disposing of element
- ?destroy = function(this),
- -- triggered when event happens and cursor is above element
- ?on_{event_name} = function(this),
- -- triggered when any event happens anywhere on a page
- ?on_global_{event_name} = function(this),
- -- object
- ?render = function(this_element),
-}
-]]
-local Element = {
- captures = nil,
- ax = 0, ay = 0, bx = 0, by = 0,
- proximity = 0, proximity_raw = infinity,
-}
-Element.__index = Element
-
-function Element.new(props)
- local element = setmetatable(props, Element)
- element._eventListeners = {}
-
- -- Flash timer
- element._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
- local getTo = function() return element.proximity end
- element:tween_property('forced_proximity', 1, getTo, function()
- element.forced_proximity = nil
- end)
- end)
- element._flash_out_timer:kill()
-
- element:init()
-
- return element
-end
-
-function Element:init() end
-function Element:destroy() end
-
--- Call method if it exists
-function Element:maybe(name, ...)
- if self[name] then return self[name](self, ...) end
-end
-
--- Tween helpers
-function Element:tween(...) tween_element(self, ...) end
-function Element:tween_property(...) tween_element_property(self, ...) end
-function Element:tween_stop() tween_element_stop(self) end
-function Element:is_tweening() tween_element_is_tweening(self) end
-
--- Event listeners
-function Element:on(name, handler)
- if self._eventListeners[name] == nil then self._eventListeners[name] = {} end
- local preexistingIndex = itable_find(self._eventListeners[name], handler)
- if preexistingIndex then
- return
- else
- self._eventListeners[name][#self._eventListeners[name] + 1] = handler
- end
-end
-function Element:off(name, handler)
- if self._eventListeners[name] == nil then return end
- local index = itable_find(self._eventListeners, handler)
- if index then table.remove(self._eventListeners, index) end
-end
-function Element:trigger(name, ...)
- self:maybe('on_'..name, ...)
- if self._eventListeners[name] == nil then return end
- for _, handler in ipairs(self._eventListeners[name]) do handler(...) end
-end
-
--- Briefly flashes the element for `options.flash_duration` milliseconds.
--- Useful to visualize changes of volume and timeline when changed via hotkeys.
--- Implemented by briefly adding animated `forced_proximity` property to the element.
-function Element:flash()
- if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
- self:tween_stop()
- self.forced_proximity = 1
- self._flash_out_timer:kill()
- self._flash_out_timer:resume()
- end
-end
-
--- ELEMENTS
-
-local Elements = {itable = {}}
-Elements.__index = Elements
-local elements = setmetatable({}, Elements)
-
-function Elements:add(name, element)
- local insert_index = #Elements.itable + 1
-
- -- Replace if element already exists
- if self:has(name) then
- insert_index = itable_find(Elements.itable, function(_, element)
- return element.name == name
- end)
- end
-
- element.name = name
- Elements.itable[insert_index] = element
- self[name] = element
-
- request_render()
-end
-
-function Elements:remove(name, props)
- Elements.itable = itable_remove(Elements.itable, self[name])
- self[name] = nil
- request_render()
-end
-
-function Elements:has(name) return self[name] ~= nil end
-function Elements:ipairs() return ipairs(Elements.itable) end
-function Elements:pairs(elements) return pairs(self) end
-
--- MENU
---[[
-Usage:
-```
-local items = {
- {title = 'Foo title', hint = 'Ctrl+F', value = 'foo'},
- {title = 'Bar title', hint = 'Ctrl+B', value = 'bar'},
- {
- title = 'Submenu',
- items = {
- {title = 'Sub item 1', value = 'sub1'},
- {title = 'Sub item 2', value = 'sub2'}
- }
- }
-}
-
-function open_item(value)
- value -- value from `item.value`
-end
-
-menu:open(items, open_item)
-```
-]]
-local Menu = {}
-Menu.__index = Menu
-local menu = setmetatable({key_bindings = {}, is_closing = false}, Menu)
-
-function Menu:is_open(menu_type)
- return elements.menu ~= nil and (not menu_type or elements.menu.type == menu_type)
-end
-
-function Menu:open(items, open_item, opts)
- opts = opts or {}
-
- if menu:is_open() then
- if not opts.parent_menu then
- menu:close(true, function()
- menu:open(items, open_item, opts)
- end)
- return
- end
- else
- menu:enable_key_bindings()
- elements.curtain:fadein()
- end
-
- elements:add('menu', Element.new({
- captures = {mouse_buttons = true},
- type = nil, -- menu type such as `menu`, `chapters`, ...
- title = nil,
- width = nil,
- height = nil,
- offset_x = 0, -- used to animated from/to left when submenu
- item_height = nil,
- item_spacing = 1,
- item_content_spacing = nil,
- font_size = nil,
- scroll_step = nil,
- scroll_height = nil,
- scroll_y = 0,
- opacity = 0,
- relative_parent_opacity = 0.4,
- items = items,
- active_item = nil,
- selected_item = nil,
- open_item = open_item,
- parent_menu = nil,
- init = function(this)
- -- Already initialized
- if this.width ~= nil then return end
-
- -- Apply options
- for key, value in pairs(opts) do this[key] = value end
- this.selected_item = this.active_item
-
- -- Set initial dimensions
- this:on_display_resize()
-
- -- Scroll to active item
- this:scroll_to_item(this.active_item)
-
- -- Transition in animation
- menu.transition = {to = 'child', target = this}
- local start_offset = this.parent_menu and (this.parent_menu.width + this.width) / 2 or 0
-
- tween_element(menu.transition.target, 0, 1, function(_, pos)
- this:set_offset_x(round(start_offset * (1 - pos)))
- this.opacity = pos
- this:set_parent_opacity(1 - ((1 - config.menu_parent_opacity) * pos))
- end, function()
- menu.transition = nil
- update_proximities()
- end)
- end,
- destroy = function(this)
- request_render()
- end,
- on_display_resize = function(this)
- this.item_height = (state.fullscreen or state.maximized) and options.menu_item_height_fullscreen or options.menu_item_height
- this.font_size = round(this.item_height * 0.48 * options.menu_font_scale)
- this.item_content_spacing = round((this.item_height - this.font_size) * 0.6)
- this.scroll_step = this.item_height + this.item_spacing
-
- -- Estimate width of a widest item
- local estimated_max_width = 0
- for _, item in ipairs(items) do
- local item_text_length = ((item.title and item.title:len() or 0) + (item.hint and item.hint:len() or 0))
- local spacings_in_item = item.hint and 3 or 2
- local estimated_width = text_width_estimate(item_text_length, this.font_size) + (this.item_content_spacing * spacings_in_item)
- if estimated_width > estimated_max_width then
- estimated_max_width = estimated_width
- end
- end
-
- -- Also check menu title
- local menu_title_length = this.title and this.title:len() or 0
- local estimated_menu_title_width = text_width_estimate(menu_title_length, this.font_size)
- if estimated_menu_title_width > estimated_max_width then
- estimated_max_width = estimated_menu_title_width
- end
-
- -- Coordinates and sizes are of the scrollable area to make
- -- consuming values in rendering easier. Title drawn above this, so
- -- we need to account for that in max_height and ay position.
- this.width = round(math.min(math.max(estimated_max_width, config.menu_min_width), display.width * 0.9))
- local title_height = this.title and this.scroll_step or 0
- local max_height = round(display.height * 0.9) - title_height
- this.height = math.min(round(this.scroll_step * #items) - this.item_spacing, max_height)
- this.scroll_height = math.max((this.scroll_step * #this.items) - this.height - this.item_spacing, 0)
- this.ax = round((display.width - this.width) / 2) + this.offset_x
- this.ay = round((display.height - this.height) / 2 + (title_height / 2))
- this.bx = round(this.ax + this.width)
- this.by = round(this.ay + this.height)
-
- if this.parent_menu then
- this.parent_menu:on_display_resize()
- end
- end,
- set_items = function(this, items, props)
- this.items = items
- this.selected_item = nil
- this.active_item = nil
- if props then
- for key, value in pairs(props) do this[key] = value end
- end
- this:on_display_resize()
- request_render()
- end,
- set_offset_x = function(this, offset)
- local delta = offset - this.offset_x
- this.offset_x = offset
- this.ax = this.ax + delta
- this.bx = this.bx + delta
- if this.parent_menu then
- this.parent_menu:set_offset_x(offset - ((this.width + this.parent_menu.width) / 2) - this.item_spacing)
- else
- update_proximities()
- end
- end,
- fadeout = function(this, callback)
- this:tween(1, 0, function(this, pos)
- this.opacity = pos
- this:set_parent_opacity(pos * config.menu_parent_opacity)
- end, callback)
- end,
- set_parent_opacity = function(this, opacity)
- if this.parent_menu then
- this.parent_menu.opacity = opacity
- this.parent_menu:set_parent_opacity(opacity * config.menu_parent_opacity)
- end
- end,
- get_item_index_below_cursor = function(this)
- return math.ceil((cursor.y - this.ay + this.scroll_y) / this.scroll_step)
- end,
- get_first_visible_index = function(this)
- return round(this.scroll_y / this.scroll_step) + 1
- end,
- get_last_visible_index = function(this)
- return round((this.scroll_y + this.height) / this.scroll_step)
- end,
- get_centermost_visible_index = function(this)
- return round((this.scroll_y + (this.height / 2)) / this.scroll_step)
- end,
- scroll_to = function(this, pos)
- this.scroll_y = math.max(math.min(pos, this.scroll_height), 0)
- request_render()
- end,
- scroll_to_item = function(this, index)
- if (index and index >= 1 and index <= #this.items) then
- this:scroll_to(round((this.scroll_step * (index - 1)) - ((this.height - this.scroll_step) / 2)))
- end
- end,
- select_index = function(this, index)
- this.selected_item = (index and index >= 1 and index <= #this.items) and index or nil
- request_render()
- end,
- select_value = function(this, value)
- this:select_index(itable_find(this.items, function(_, item) return item.value == value end))
- end,
- activate_index = function(this, index)
- this.active_item = (index and index >= 1 and index <= #this.items) and index or nil
- request_render()
- end,
- activate_value = function(this, value)
- this:activate_index(itable_find(this.items, function(_, item) return item.value == value end))
- end,
- delete_index = function(this, index)
- if (index and index >= 1 and index <= #this.items) then
- local previous_active_value = this.active_index and this.items[this.active_index].value or nil
- table.remove(this.items, index)
- this:on_display_resize()
- if previous_active_value then this:activate_value(previous_active_value) end
- this:scroll_to_item(this.selected_item)
- end
- end,
- delete_value = function(this, value)
- this:delete_index(itable_find(this.items, function(_, item) return item.value == value end))
- end,
- prev = function(this)
- local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_last_visible_index()
- local current_index = this.selected_item or default_anchor + 1
- this.selected_item = math.max(current_index - 1, 1)
- this:scroll_to_item(this.selected_item)
- end,
- next = function(this)
- local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_first_visible_index()
- local current_index = this.selected_item or default_anchor - 1
- this.selected_item = math.min(current_index + 1, #this.items)
- this:scroll_to_item(this.selected_item)
- end,
- back = function(this)
- if menu.transition then
- local transition_target = menu.transition.target
- local transition_target_type = menu.transition.target
- tween_element_stop(transition_target)
- if transition_target_type == 'parent' then
- elements:add('menu', transition_target)
- end
- menu.transition = nil
- transition_target:back()
- return
- else
- menu.transition = {to = 'parent', target = this.parent_menu}
- end
-
- if menu.transition.target == nil then
- menu:close()
- return
- end
-
- local target = menu.transition.target
- local to_offset = -target.offset_x + this.offset_x
-
- tween_element(target, 0, 1, function(_, pos)
- this:set_offset_x(round(to_offset * pos))
- this.opacity = 1 - pos
- this:set_parent_opacity(config.menu_parent_opacity + ((1 - config.menu_parent_opacity) * pos))
- end, function()
- menu.transition = nil
- elements:add('menu', target)
- update_proximities()
- end)
- end,
- open_selected_item = function(this)
- -- If there is a transition active and this method got called, it
- -- means we are animating from this menu to parent menu, and all
- -- calls to this method should be relayed to the parent menu.
- if menu.transition and menu.transition.to == 'parent' then
- local target = menu.transition.target
- tween_element_stop(target)
- menu.transition = nil
- target:open_selected_item()
- return
- end
-
- if this.selected_item then
- local item = this.items[this.selected_item]
- -- Is submenu
- if item.items then
- local opts = table_copy(opts)
- opts.parent_menu = this
- menu:open(item.items, this.open_item, opts)
- else
- menu:close(true)
- this.open_item(item.value)
- end
- end
- end,
- close = function(this)
- menu:close()
- end,
- on_global_mbtn_left_down = function(this)
- if this.proximity_raw == 0 then
- this.selected_item = this:get_item_index_below_cursor()
- this:open_selected_item()
- else
- -- check if this is clicking on any parent menus
- local parent_menu = this.parent_menu
- repeat
- if parent_menu then
- if get_point_to_rectangle_proximity(cursor, parent_menu) == 0 then
- this:back()
- return
- end
- parent_menu = parent_menu.parent_menu
- end
- until parent_menu == nil
-
- menu:close()
- end
- end,
- on_global_mouse_move = function(this)
- if this.proximity_raw == 0 then
- this.selected_item = this:get_item_index_below_cursor()
- else
- if this.selected_item then this.selected_item = nil end
- end
- request_render()
- end,
- on_wheel_up = function(this)
- this.selected_item = nil
- this:scroll_to(this.scroll_y - this.scroll_step)
- -- Selects item below cursor
- this:on_global_mouse_move()
- request_render()
- end,
- on_wheel_down = function(this)
- this.selected_item = nil
- this:scroll_to(this.scroll_y + this.scroll_step)
- -- Selects item below cursor
- this:on_global_mouse_move()
- request_render()
- end,
- on_pgup = function(this)
- this.selected_item = nil
- this:scroll_to(this.scroll_y - this.height)
- end,
- on_pgdwn = function(this)
- this.selected_item = nil
- this:scroll_to(this.scroll_y + this.height)
- end,
- on_home = function(this)
- this.selected_item = nil
- this:scroll_to(0)
- end,
- on_end = function(this)
- this.selected_item = nil
- this:scroll_to(this.scroll_height)
- end,
- render = render_menu,
- }))
-
- elements.menu:maybe('on_open')
-end
-
-function Menu:add_key_binding(key, name, fn, flags)
- menu.key_bindings[#menu.key_bindings + 1] = name
- mp.add_forced_key_binding(key, name, fn, flags)
-end
-
-function Menu:enable_key_bindings()
- menu.key_bindings = {}
- -- The `mp.set_key_bindings()` method would be easier here, but that
- -- doesn't support 'repeatable' flag, so we are stuck with this monster.
- menu:add_key_binding('up', 'menu-prev', self:create_action('prev'), 'repeatable')
- menu:add_key_binding('down', 'menu-next', self:create_action('next'), 'repeatable')
- menu:add_key_binding('left', 'menu-back', self:create_action('back'))
- menu:add_key_binding('right', 'menu-select', self:create_action('open_selected_item'))
-
- if options.menu_wasd_navigation then
- menu:add_key_binding('w', 'menu-prev-alt', self:create_action('prev'), 'repeatable')
- menu:add_key_binding('a', 'menu-back-alt', self:create_action('back'))
- menu:add_key_binding('s', 'menu-next-alt', self:create_action('next'), 'repeatable')
- menu:add_key_binding('d', 'menu-select-alt', self:create_action('open_selected_item'))
- end
-
- if options.menu_hjkl_navigation then
- menu:add_key_binding('h', 'menu-back-alt2', self:create_action('back'))
- menu:add_key_binding('j', 'menu-next-alt2', self:create_action('next'), 'repeatable')
- menu:add_key_binding('k', 'menu-prev-alt2', self:create_action('prev'), 'repeatable')
- menu:add_key_binding('l', 'menu-select-alt2', self:create_action('open_selected_item'))
- end
-
- menu:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_action('back'))
- menu:add_key_binding('bs', 'menu-back-alt4', self:create_action('back'))
- menu:add_key_binding('enter', 'menu-select-alt3', self:create_action('open_selected_item'))
- menu:add_key_binding('kp_enter', 'menu-select-alt4', self:create_action('open_selected_item'))
- menu:add_key_binding('esc', 'menu-close', self:create_action('close'))
- menu:add_key_binding('pgup', 'menu-page-up', self:create_action('on_pgup'))
- menu:add_key_binding('pgdwn', 'menu-page-down', self:create_action('on_pgdwn'))
- menu:add_key_binding('home', 'menu-home', self:create_action('on_home'))
- menu:add_key_binding('end', 'menu-end', self:create_action('on_end'))
-end
-
-function Menu:disable_key_bindings()
- for _, name in ipairs(menu.key_bindings) do mp.remove_key_binding(name) end
- menu.key_bindings = {}
-end
-
-function Menu:create_action(name)
- return function(...)
- if elements.menu then elements.menu:maybe(name, ...) end
- end
-end
-
-function Menu:close(immediate, callback)
- if type(immediate) ~= 'boolean' then callback = immediate end
-
- if elements:has('menu') and not menu.is_closing then
- function close()
- elements.menu:maybe('on_close')
- elements.menu:destroy()
- elements:remove('menu')
- menu.is_closing = false
- update_proximities()
- menu:disable_key_bindings()
- call_me_maybe(callback)
- end
-
- menu.is_closing = true
- elements.curtain:fadeout()
-
- if immediate then
- close()
- else
- elements.menu:fadeout(close)
- end
- end
-end
-
--- ICONS
---[[
-ASS \shadN shadows are drawn also below the element, which when there is an
-opacity in play, blends icon colors into ugly greys. The mess below is an
-attempt to fix it by rendering shadows for icons with clipping.
-
-Add icons by adding functions to render them to `icons` table.
-
-Signature: function(pos_x, pos_y, size) => string
-
-Function has to return ass path coordinates to draw the icon centered at pox_x
-and pos_y of passed size.
-]]
-local icons = {}
-function icon(name, icon_x, icon_y, icon_size, shad_x, shad_y, shad_size, backdrop, opacity, clip)
- local ass = assdraw.ass_new()
- local icon_path = icons[name](icon_x, icon_y, icon_size)
- local icon_color = options['color_'..backdrop..'_text']
- local shad_color = options['color_'..backdrop]
- local use_border = (shad_x + shad_y) == 0
- local icon_border = use_border and shad_size or 0
-
- -- clip can't clip out shadows, a very annoying limitation I can't work
- -- around without going back to ugly default ass shadows, but atm I actually
- -- don't need clipping of icons with shadows, so I'm choosing to ignore this
- if not clip then
- clip = ''
- end
-
- if not use_border then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad0\\1c&H'..shad_color..'\\iclip('..ass.scale..', '..icon_path..')}')
- ass:append(ass_opacity(opacity))
- ass:pos(shad_x + shad_size, shad_y + shad_size)
- ass:draw_start()
- ass:append(icon_path)
- ass:draw_stop()
- end
-
- ass:new_event()
- ass:append('{\\blur0\\bord'..icon_border..'\\shad0\\1c&H'..icon_color..'\\3c&H'..shad_color..clip..'}')
- ass:append(ass_opacity(opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:append(icon_path)
- ass:draw_stop()
-
- return ass.text
-end
-
-function icons._volume(muted, pos_x, pos_y, size)
- local ass = assdraw.ass_new()
- local scale = size / 200
- function x(number) return pos_x + (number * scale) end
- function y(number) return pos_y + (number * scale) end
- ass:move_to(x(-85), y(-35))
- ass:line_to(x(-50), y(-35))
- ass:line_to(x(-5), y(-75))
- ass:line_to(x(-5), y(75))
- ass:line_to(x(-50), y(35))
- ass:line_to(x(-85), y(35))
- if muted then
- ass:move_to(x(76), y(-35)) ass:line_to(x(50), y(-9)) ass:line_to(x(24), y(-35))
- ass:line_to(x(15), y(-26)) ass:line_to(x(41), y(0)) ass:line_to(x(15), y(26))
- ass:line_to(x(24), y(35)) ass:line_to(x(50), y(9)) ass:line_to(x(76), y(35))
- ass:line_to(x(85), y(26)) ass:line_to(x(59), y(0)) ass:line_to(x(85), y(-26))
- else
- ass:move_to(x(20), y(-30)) ass:line_to(x(20), y(30))
- ass:line_to(x(35), y(30)) ass:line_to(x(35), y(-30))
-
- ass:move_to(x(55), y(-60)) ass:line_to(x(55), y(60))
- ass:line_to(x(70), y(60)) ass:line_to(x(70), y(-60))
- end
- return ass.text
-end
-function icons.volume(pos_x, pos_y, size) return icons._volume(false, pos_x, pos_y, size) end
-function icons.volume_muted(pos_x, pos_y, size) return icons._volume(true, pos_x, pos_y, size) end
-
-function icons.arrow_right(pos_x, pos_y, size)
- local ass = assdraw.ass_new()
- local scale = size / 200
- function x(number) return pos_x + (number * scale) end
- function y(number) return pos_y + (number * scale) end
- ass:move_to(x(-22), y(-80))
- ass:line_to(x(-45), y(-57))
- ass:line_to(x(12), y(0))
- ass:line_to(x(-45), y(57))
- ass:line_to(x(-22), y(80))
- ass:line_to(x(58), y(0))
- return ass.text
-end
-
--- STATE UPDATES
-
-function update_display_dimensions()
- local o = mp.get_property_native('osd-dimensions')
- display.width = o.w
- display.height = o.h
- display.aspect = o.aspect
-
- -- Tell elements about this
- for _, element in elements:ipairs() do
- if element.on_display_resize ~= nil then
- element.on_display_resize(element)
- end
- end
-end
-
-function update_element_cursor_proximity(element)
- if cursor.hidden then
- element.proximity_raw = infinity
- element.proximity = 0
- else
- local range = options.proximity_out - options.proximity_in
- element.proximity_raw = get_point_to_rectangle_proximity(cursor, element)
- element.proximity = menu:is_open() and 0 or 1 - (math.min(math.max(element.proximity_raw - options.proximity_in, 0), range) / range)
- end
-end
-
-function update_proximities()
- local capture_mouse_buttons = false
- local capture_wheel = false
- local menu_only = menu:is_open()
- local mouse_left_elements = {}
- local mouse_entered_elements = {}
-
- -- Calculates proximities and opacities for defined elements
- for _, element in elements:ipairs() do
- local previous_proximity_raw = element.proximity_raw
-
- -- If menu is open, all other elements have to be disabled
- if menu_only then
- if element.name == 'menu' then
- capture_mouse_buttons = true
- capture_wheel = true
- update_element_cursor_proximity(element)
- else
- element.proximity_raw = infinity
- element.proximity = 0
- end
- else
- update_element_cursor_proximity(element)
- end
-
- if element.proximity_raw == 0 then
- -- Mouse is over element
- if element.captures and element.captures.mouse_buttons then capture_mouse_buttons = true end
- if element.captures and element.captures.wheel then capture_wheel = true end
-
- -- Mouse entered element area
- if previous_proximity_raw ~= 0 then
- mouse_entered_elements[#mouse_entered_elements + 1] = element
- end
- else
- -- Mouse left element area
- if previous_proximity_raw == 0 then
- mouse_left_elements[#mouse_left_elements + 1] = element
- end
- end
- end
-
- -- Enable key group captures elements request.
- if capture_mouse_buttons then
- forced_key_bindings.mouse_buttons:enable()
- else
- forced_key_bindings.mouse_buttons:disable()
- end
- if capture_wheel then
- forced_key_bindings.wheel:enable()
- else
- forced_key_bindings.wheel:disable()
- end
-
- -- Trigger `mouse_leave` and `mouse_enter` events
- for _, element in ipairs(mouse_left_elements) do element:trigger('mouse_leave') end
- for _, element in ipairs(mouse_entered_elements) do element:trigger('mouse_enter') end
-end
-
--- ELEMENT RENDERERS
-
-function render_timeline(this)
- if this.size_max == 0 or state.duration == nil or state.duration == 0 or state.position == nil then return end
-
- local size_min = this:get_effective_size_min()
- local size = this:get_effective_size()
-
- if size < 1 then return end
-
- local ass = assdraw.ass_new()
-
- -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
- local hide_text_below = math.max(this.font_size * 0.7, size_min * 2)
- local hide_text_ramp = hide_text_below / 2
- local text_opacity = math.max(math.min(size - hide_text_below, hide_text_ramp), 0) / hide_text_ramp
-
- local spacing = math.max(math.floor((this.size_max - this.font_size) / 2.5), 4)
- local progress = state.position / state.duration
-
- -- Background bar coordinates
- local bax = 0
- local bay = display.height - size - this.bottom_border - this.top_border
- local bbx = display.width
- local bby = display.height
-
- -- Foreground bar coordinates
- local fax = bax
- local fay = bay + this.top_border
- local fbx = bbx * progress
- local fby = bby - this.bottom_border
- local foreground_size = bby - bay
- local foreground_coordinates = fax..','..fay..','..fbx..','..fby -- for clipping
-
- -- Background
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..foreground_coordinates..')}')
- ass:append(ass_opacity(math.max(options.timeline_opacity - 0.1, 0)))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(bax, bay, bbx, bby)
- ass:draw_stop()
-
- -- Foreground
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
- ass:append(ass_opacity(options.timeline_opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(fax, fay, fbx, fby)
- ass:draw_stop()
-
- -- Seekable ranges
- if options.timeline_cached_ranges and state.cached_ranges then
- local range_height = math.max(foreground_size / 8, size_min)
- local range_ay = fby - range_height
- for _, range in ipairs(state.cached_ranges) do
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.timeline_cached_ranges.color..'}')
- ass:append(ass_opacity(options.timeline_cached_ranges.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(
- bbx * (range['start'] / state.duration), range_ay,
- bbx * (range['end'] / state.duration), range_ay + range_height
- )
- ass:draw_stop()
- end
- end
-
- -- Custom ranges
- if state.chapter_ranges ~= nil then
- for i, chapter_range in ipairs(state.chapter_ranges) do
- for i, range in ipairs(chapter_range.ranges) do
- local rax = display.width * (range['start'].time / state.duration)
- local rbx = display.width * (range['end'].time / state.duration)
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..chapter_range.color..'}')
- ass:append(ass_opacity(chapter_range.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- -- for 1px chapter size, use the whole size of the bar including padding
- if size <= 1 then
- ass:rect_cw(rax, bay, rbx, bby)
- else
- ass:rect_cw(rax, fay, rbx, fby)
- end
- ass:draw_stop()
- end
- end
- end
-
- -- Chapters
- if options.chapters ~= 'none' and state.chapters ~= nil and #state.chapters > 0 then
- local half_size = size / 2
- local dots = false
- local chapter_size, chapter_y
- if options.chapters == 'dots' then
- dots = true
- chapter_size = math.min(6, (foreground_size / 2) + 2)
- chapter_y = math.min(fay + chapter_size, fay + half_size)
- elseif options.chapters == 'lines' then
- chapter_size = size
- chapter_y = fay + (chapter_size / 2)
- elseif options.chapters == 'lines-top' then
- chapter_size = math.min(this.size_max / 3.5, size)
- chapter_y = fay + (chapter_size / 2)
- elseif options.chapters == 'lines-bottom' then
- chapter_size = math.min(this.size_max / 3.5, size)
- chapter_y = fay + size - (chapter_size / 2)
- end
-
- if chapter_size ~= nil then
- -- for 1px chapter size, use the whole size of the bar including padding
- chapter_size = size <= 1 and foreground_size or chapter_size
- local chapter_half_size = chapter_size / 2
-
- for i, chapter in ipairs(state.chapters) do
- local chapter_x = display.width * (chapter.time / state.duration)
- local color = chapter_x > fbx and options.color_foreground or options.color_background
-
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..color..'}')
- ass:append(ass_opacity(options.chapters_opacity))
- ass:pos(0, 0)
- ass:draw_start()
-
- if dots then
- local bezier_stretch = chapter_size * 0.67
- ass:move_to(chapter_x - chapter_half_size, chapter_y)
- ass:bezier_curve(
- chapter_x - chapter_half_size, chapter_y - bezier_stretch,
- chapter_x + chapter_half_size, chapter_y - bezier_stretch,
- chapter_x + chapter_half_size, chapter_y
- )
- ass:bezier_curve(
- chapter_x + chapter_half_size, chapter_y + bezier_stretch,
- chapter_x - chapter_half_size, chapter_y + bezier_stretch,
- chapter_x - chapter_half_size, chapter_y
- )
- else
- ass:rect_cw(chapter_x, chapter_y - chapter_half_size, chapter_x + 1, chapter_y + chapter_half_size)
- end
-
- ass:draw_stop()
- end
- end
- end
-
- if text_opacity > 0 then
- -- Elapsed time
- if state.elapsed_seconds then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
- ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
- ass:pos(spacing, fay + (size / 2))
- ass:an(4)
- ass:append(state.elapsed_time)
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
- ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
- ass:pos(spacing, fay + (size / 2))
- ass:an(4)
- ass:append(state.elapsed_time)
- end
-
- -- Remaining time
- if state.remaining_seconds then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
- ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
- ass:pos(display.width - spacing, fay + (size / 2))
- ass:an(6)
- ass:append('-'..state.remaining_time)
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
- ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
- ass:pos(display.width - spacing, fay + (size / 2))
- ass:an(6)
- ass:append('-'..state.remaining_time)
- end
- end
-
- if (this.proximity_raw == 0 or this.pressed) and not (elements.speed and elements.speed.dragging) then
- -- Hovered time
- local hovered_seconds = state.duration * (cursor.x / display.width)
- local box_half_width_guesstimate = (this.font_size * 4.2) / 2
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'')
- ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1)))
- ass:pos(math.min(math.max(cursor.x, box_half_width_guesstimate), display.width - box_half_width_guesstimate), fay)
- ass:an(2)
- ass:append(mp.format_time(hovered_seconds))
-
- -- Cursor line
- ass:new_event()
- ass:append('{\\blur0\\bord0\\xshad-1\\yshad0\\1c&H'..options.color_foreground..'\\4c&H'..options.color_background..'}')
- ass:append(ass_opacity(0.2))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(cursor.x, fay, cursor.x + 1, fby)
- ass:draw_stop()
- end
-
- return ass
-end
-
-function render_top_bar(this)
- local opacity = this:get_effective_proximity()
-
- if not this.enabled or opacity == 0 then return end
-
- local ass = assdraw.ass_new()
-
- if options.top_bar_controls then
- -- Close button
- local close = elements.window_controls_close
- if close.proximity_raw == 0 then
- -- Background on hover
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H2311e8}')
- ass:append(ass_opacity(this.button_opacity, opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(close.ax, close.ay, close.bx, close.by)
- ass:draw_stop()
- end
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
- ass:append(ass_opacity(this.button_opacity, opacity))
- ass:pos(close.ax + (this.button_width / 2), (this.size / 2))
- ass:draw_start()
- ass:move_to(-this.icon_size, this.icon_size)
- ass:line_to(this.icon_size, -this.icon_size)
- ass:move_to(-this.icon_size, -this.icon_size)
- ass:line_to(this.icon_size, this.icon_size)
- ass:draw_stop()
-
- -- Maximize button
- local maximize = elements.window_controls_maximize
- if maximize.proximity_raw == 0 then
- -- Background on hover
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H222222}')
- ass:append(ass_opacity(this.button_opacity, opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(maximize.ax, maximize.ay, maximize.bx, maximize.by)
- ass:draw_stop()
- end
- ass:new_event()
- ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&H000000}')
- ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
- ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2))
- ass:draw_start()
- ass:rect_cw(-this.icon_size + 1, -this.icon_size + 1, this.icon_size + 1, this.icon_size + 1)
- ass:draw_stop()
- ass:new_event()
- ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&HFFFFFF}')
- ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
- ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2))
- ass:draw_start()
- ass:rect_cw(-this.icon_size, -this.icon_size, this.icon_size, this.icon_size)
- ass:draw_stop()
-
- -- Minimize button
- local minimize = elements.window_controls_minimize
- if minimize.proximity_raw == 0 then
- -- Background on hover
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H222222}')
- ass:append(ass_opacity(this.button_opacity, opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(minimize.ax, minimize.ay, minimize.bx, minimize.by)
- ass:draw_stop()
- end
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
- ass:append(ass_opacity(this.button_opacity, opacity))
- ass:append('{\\1a&HFF&}')
- ass:pos(minimize.ax + (this.button_width / 2), (this.size / 2))
- ass:draw_start()
- ass:move_to(-this.icon_size, 0)
- ass:line_to(this.icon_size, 0)
- ass:draw_stop()
- end
-
- -- Window title
- if options.top_bar_title and state.media_title then
- local clip_coordinates = '0,0,'..(this.title_bx - this.spacing)..','..this.size
-
- ass:new_event()
- ass:append('{\\q2\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..clip_coordinates..')')
- ass:append(ass_opacity(1, opacity))
- ass:pos(0 + this.spacing, this.size / 2)
- ass:an(4)
- ass:append(state.media_title)
- end
-
- return ass
-end
-
-function render_volume(this)
- local slider = elements.volume_slider
- local opacity = this:get_effective_proximity()
-
- if this.width == 0 or opacity == 0 then return end
-
- local ass = assdraw.ass_new()
-
- if slider.height > 0 then
- -- Background bar coordinates
- local bax = slider.ax
- local bay = slider.ay
- local bbx = slider.bx
- local bby = slider.by
-
- -- Foreground bar coordinates
- local height_without_border = slider.height - (options.volume_border * 2)
- local fax = slider.ax + options.volume_border
- local fay = slider.ay + (height_without_border * (1 - math.min(state.volume / state.volume_max, 1))) + options.volume_border
- local fbx = slider.bx - options.volume_border
- local fby = slider.by - options.volume_border
-
- -- Path to draw a foreground bar with a 100% volume indicator, already
- -- clipped by volume level. Can't just clip it with rectangle, as it itself
- -- also needs to be used as a path to clip the background bar and volume
- -- number.
- local fpath = assdraw.ass_new()
- fpath:move_to(fbx, fby)
- fpath:line_to(fax, fby)
- local nudge_bottom_y = slider.nudge_y + slider.nudge_size
- if fay <= nudge_bottom_y and slider.draw_nudge then
- fpath:line_to(fax, math.min(nudge_bottom_y))
- if fay <= slider.nudge_y then
- fpath:line_to((fax + slider.nudge_size), slider.nudge_y)
- local nudge_top_y = slider.nudge_y - slider.nudge_size
- if fay <= nudge_top_y then
- fpath:line_to(fax, nudge_top_y)
- fpath:line_to(fax, fay)
- fpath:line_to(fbx, fay)
- fpath:line_to(fbx, nudge_top_y)
- else
- local triangle_side = fay - nudge_top_y
- fpath:line_to((fax + triangle_side), fay)
- fpath:line_to((fbx - triangle_side), fay)
- end
- fpath:line_to((fbx - slider.nudge_size), slider.nudge_y)
- else
- local triangle_side = nudge_bottom_y - fay
- fpath:line_to((fax + triangle_side), fay)
- fpath:line_to((fbx - triangle_side), fay)
- end
- fpath:line_to(fbx, nudge_bottom_y)
- else
- fpath:line_to(fax, fay)
- fpath:line_to(fbx, fay)
- end
- fpath:line_to(fbx, fby)
-
- -- Background
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..fpath.scale..', '..fpath.text..')}')
- ass:append(ass_opacity(math.max(options.volume_opacity - 0.1, 0), opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:move_to(bax, bay)
- ass:line_to(bbx, bay)
- local half_border = options.volume_border / 2
- if slider.draw_nudge then
- ass:line_to(bbx, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
- ass:line_to(bbx - slider.nudge_size + half_border, slider.nudge_y)
- ass:line_to(bbx, slider.nudge_y + slider.nudge_size - half_border)
- end
- ass:line_to(bbx, bby)
- ass:line_to(bax, bby)
- if slider.draw_nudge then
- ass:line_to(bax, slider.nudge_y + slider.nudge_size - half_border)
- ass:line_to(bax + slider.nudge_size - half_border, slider.nudge_y)
- ass:line_to(bax, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
- end
- ass:line_to(bax, bay)
- ass:draw_stop()
-
- -- Foreground
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
- ass:append(ass_opacity(options.volume_opacity, opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:append(fpath.text)
- ass:draw_stop()
-
- -- Current volume value
- local volume_string = tostring(round(state.volume * 10) / 10)
- local font_size = round(((this.width * 0.6) - (#volume_string * (this.width / 20))) * options.volume_font_scale)
- if fay < slider.by - slider.spacing then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\clip('..fpath.scale..', '..fpath.text..')}')
- ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
- ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
- ass:an(2)
- ass:append(volume_string)
- end
- if fay > slider.by - slider.spacing - font_size then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\iclip('..fpath.scale..', '..fpath.text..')}')
- ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
- ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
- ass:an(2)
- ass:append(volume_string)
- end
- end
-
- -- Mute button
- local mute = elements.volume_mute
- local icon_name = state.mute and 'volume_muted' or 'volume'
- ass:new_event()
- ass:append(icon(
- icon_name,
- mute.ax + (mute.width / 2), mute.ay + (mute.height / 2), mute.width * 0.7, -- x, y, size
- 0, 0, options.volume_border, -- shadow_x, shadow_y, shadow_size
- 'background', options.volume_opacity * opacity -- backdrop, opacity
- ))
- return ass
-end
-
-function render_speed(this)
- if not this.dragging and (elements.curtain.opacity > 0) then return end
-
- local timeline = elements.timeline
- local proximity = timeline:get_effective_proximity()
- local opacity = this.forced_proximity and this.forced_proximity or (this.dragging and 1 or proximity)
-
- if opacity == 0 then return end
-
- local ass = assdraw.ass_new()
-
- -- Coordinates
- local ax = this.ax
- local ay = this.ay + timeline.size_max - timeline:get_effective_size() - timeline.top_border - timeline.bottom_border
- local bx = this.bx
- local by = ay + this.height
- local half_width = (this.width / 2)
- local half_x = ax + half_width
-
- -- Notches
- local speed_at_center = state.speed
- if this.dragging then
- speed_at_center = this.dragging.start_speed + ((-this.dragging.distance / this.step_distance) * options.speed_step)
- speed_at_center = math.min(math.max(speed_at_center, 0.01), 100)
- end
- local nearest_notch_speed = round(speed_at_center / this.notch_every) * this.notch_every
- local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / this.notch_every) * this.notch_spacing)
- local guide_size = math.floor(this.height / 7.5)
- local notch_by = by - guide_size
- local notch_ay_big = ay + round(this.font_size * 1.1)
- local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
- local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
- local from_to_index = math.floor(this.notches / 2)
-
- for i = -from_to_index, from_to_index do
- local notch_speed = nearest_notch_speed + (i * this.notch_every)
-
- if notch_speed < 0 or notch_speed > 100 then goto continue end
-
- local notch_x = nearest_notch_x + (i * this.notch_spacing)
- local notch_thickness = 1
- local notch_ay = notch_ay_small
- if (notch_speed % (this.notch_every * 10)) < 0.00000001 then
- notch_ay = notch_ay_big
- notch_thickness = 1
- elseif (notch_speed % (this.notch_every * 5)) < 0.00000001 then
- notch_ay = notch_ay_medium
- end
-
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
- ass:append(ass_opacity(math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1), opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:move_to(notch_x - notch_thickness, notch_ay)
- ass:line_to(notch_x + notch_thickness, notch_ay)
- ass:line_to(notch_x + notch_thickness, notch_by)
- ass:line_to(notch_x - notch_thickness, notch_by)
- ass:draw_stop()
-
- ::continue::
- end
-
- -- Center guide
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
- ass:append(ass_opacity(options.speed_opacity, opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:move_to(half_x, by - 2 - guide_size)
- ass:line_to(half_x + guide_size, by - 2)
- ass:line_to(half_x - guide_size, by - 2)
- ass:draw_stop()
-
- -- Speed value
- local speed_text = (round(state.speed * 100) / 100)..'x'
- ass:new_event()
- ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'}')
- ass:append(ass_opacity(options.speed_opacity, opacity))
- ass:pos(half_x, ay)
- ass:an(8)
- ass:append(speed_text)
-
- return ass
-end
-
-function render_menu(this)
- local ass = assdraw.ass_new()
-
- if this.parent_menu then
- ass:merge(this.parent_menu:render())
- end
-
- -- Menu title
- if this.title then
- -- Background
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
- ass:append(ass_opacity(options.menu_opacity, this.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(this.ax, this.ay - this.item_height, this.bx, this.ay - 1)
- ass:draw_stop()
-
- -- Title
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad1\\b1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..'\\q2\\clip('..this.ax..','..this.ay - this.item_height..','..this.bx..','..this.ay..')}')
- ass:append(ass_opacity(options.menu_opacity, this.opacity))
- ass:pos(display.width / 2, this.ay - (this.item_height * 0.5))
- ass:an(5)
- ass:append(this.title)
- end
-
- local scroll_area_clip = '\\clip('..this.ax..','..this.ay..','..this.bx..','..this.by..')'
-
- for index, item in ipairs(this.items) do
- local item_ay = this.ay - this.scroll_y + (this.item_height * (index - 1) + this.item_spacing * (index - 1))
- local item_by = item_ay + this.item_height
- local item_clip = ''
-
- -- Clip items overflowing scroll area
- if item_ay <= this.ay or item_by >= this.by then
- item_clip = scroll_area_clip
- end
-
- if item_by < this.ay or item_ay > this.by then goto continue end
-
- local is_active = this.active_item == index
- local font_color, background_color, ass_shadow, ass_shadow_color
- local icon_size = this.font_size
-
- if is_active then
- font_color, background_color = options.color_foreground_text, options.color_foreground
- ass_shadow, ass_shadow_color = '\\shad0', ''
- else
- font_color, background_color = options.color_background_text, options.color_background
- ass_shadow, ass_shadow_color = '\\shad1', '\\4c&H'..background_color
- end
-
- local has_submenu = item.items ~= nil
- local hint_width = 0
- if item.hint then
- hint_width = text_width_estimate(item.hint:len(), this.font_size) + this.item_content_spacing
- elseif has_submenu then
- hint_width = icon_size + this.item_content_spacing
- end
-
- -- Background
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..background_color..item_clip..'}')
- ass:append(ass_opacity(options.menu_opacity, this.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(this.ax, item_ay, this.bx, item_by)
- ass:draw_stop()
-
- -- Selected highlight
- if this.selected_item == index then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..item_clip..'}')
- ass:append(ass_opacity(0.1, this.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(this.ax, item_ay, this.bx, item_by)
- ass:draw_stop()
- end
-
- -- Title
- if item.title then
- item.ass_save_title = item.ass_save_title or item.title:gsub("([{}])","\\%1")
- local title_clip_x = (this.bx - hint_width - this.item_content_spacing)
- local title_clip = '\\clip('..this.ax..','..math.max(item_ay, this.ay)..','..title_clip_x..','..math.min(item_by, this.by)..')'
- ass:new_event()
- ass:append('{\\blur0\\bord0\\shad1\\1c&H'..font_color..'\\4c&H'..background_color..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..title_clip..'\\q2}')
- ass:append(ass_opacity(options.menu_opacity, this.opacity))
- ass:pos(this.ax + this.item_content_spacing, item_ay + (this.item_height / 2))
- ass:an(4)
- ass:append(item.ass_save_title)
- end
-
- -- Hint
- if item.hint then
- item.ass_save_hint = item.ass_save_hint or item.hint:gsub("([{}])","\\%1")
- ass:new_event()
- ass:append('{\\blur0\\bord0'..ass_shadow..'\\1c&H'..font_color..''..ass_shadow_color..'\\fn'..config.font..'\\fs'..(this.font_size - 1)..bold_tag..item_clip..'}')
- ass:append(ass_opacity(options.menu_opacity * (has_submenu and 1 or 0.5), this.opacity))
- ass:pos(this.bx - this.item_content_spacing, item_ay + (this.item_height / 2))
- ass:an(6)
- ass:append(item.ass_save_hint)
- elseif has_submenu then
- ass:new_event()
- ass:append(icon(
- 'arrow_right',
- this.bx - this.item_content_spacing - (icon_size / 2), -- x
- item_ay + (this.item_height / 2), -- y
- icon_size, -- size
- 0, 0, 1, -- shadow_x, shadow_y, shadow_size
- is_active and 'foreground' or 'background', this.opacity, -- backdrop, opacity
- item_clip
- ))
- end
-
- ::continue::
- end
-
- -- Scrollbar
- if this.scroll_height > 0 then
- local groove_height = this.height - 2
- local thumb_height = math.max((this.height / (this.scroll_height + this.height)) * groove_height, 40)
- local thumb_y = this.ay + 1 + ((this.scroll_y / this.scroll_height) * (groove_height - thumb_height))
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
- ass:append(ass_opacity(options.menu_opacity, this.opacity * 0.8))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(this.bx - 3, thumb_y, this.bx - 1, thumb_y + thumb_height)
- ass:draw_stop()
- end
-
- return ass
-end
-
--- MAIN RENDERING
-
--- Request that render() is called.
--- The render is then either executed immediately, or rate-limited if it was
--- called a small time ago.
-function request_render()
- if state.render_timer == nil then
- state.render_timer = mp.add_timeout(0, render)
- end
-
- if not state.render_timer:is_enabled() then
- local now = mp.get_time()
- local timeout = config.render_delay - (now - state.render_last_time)
- if timeout < 0 then
- timeout = 0
- end
- state.render_timer.timeout = timeout
- state.render_timer:resume()
- end
-end
-
-function render()
- state.render_last_time = mp.get_time()
-
- -- Actual rendering
- local ass = assdraw.ass_new()
-
- for _, element in elements.ipairs() do
- local result = element:maybe('render')
- if result then
- ass:new_event()
- ass:merge(result)
- end
- end
-
- -- submit
- if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then
- return
- end
-
- osd.res_x = display.width
- osd.res_y = display.height
- osd.data = ass.text
- osd.z = 2000
- osd:update()
-end
-
--- STATIC ELEMENTS
-
-if itable_find({'flash', 'static'}, options.pause_indicator) then
- elements:add('pause_indicator', Element.new({
- base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8,
- paused = false,
- is_flash = options.pause_indicator == 'flash',
- is_static = options.pause_indicator == 'static',
- opacity = 0,
- init = function(this)
- local initial_call = true
- mp.observe_property('pause', 'bool', function(_, paused)
- if initial_call then
- initial_call = false
- return
- end
-
- this.paused = paused
-
- if options.pause_indicator == 'flash' then
- this.opacity = 1
- this:tween_property('opacity', 1, 0, 0.15)
- else
- this.opacity = paused and 1 or 0
- request_render()
- end
-
- end)
- end,
- render = function(this)
- if this.opacity == 0 then return end
-
- local ass = assdraw.ass_new()
-
- -- Background fadeout
- if this.is_static then
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
- ass:append(ass_opacity(0.3, this.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(0, 0, display.width, display.height)
- ass:draw_stop()
- end
-
- -- Icon
- local size = round((math.min(display.width, display.height) * (this.is_static and 0.20 or 0.15)) / 2)
-
- size = size + size * (1 - this.opacity)
-
- if this.paused then
- ass:new_event()
- ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
- ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
- ass:pos(display.width / 2, display.height / 2)
- ass:draw_start()
- ass:rect_cw(-size, -size, -size / 3, size)
- ass:draw_stop()
-
- ass:new_event()
- ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
- ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
- ass:pos(display.width / 2, display.height / 2)
- ass:draw_start()
- ass:rect_cw(size / 3, -size, size, size)
- ass:draw_stop()
- elseif this.is_flash then
- ass:new_event()
- ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
- ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
- ass:pos(display.width / 2, display.height / 2)
- ass:draw_start()
- ass:move_to(-size * 0.6, -size)
- ass:line_to(size, 0)
- ass:line_to(-size * 0.6, size)
- ass:draw_stop()
- end
-
- return ass
- end
- }))
-end
-elements:add('timeline', Element.new({
- captures = {mouse_buttons = true, wheel = true},
- pressed = false,
- size_max = 0, size_min = 0, -- set in `on_display_resize` handler based on `state.fullscreen`
- size_min_override = options.timeline_start_hidden and 0 or nil, -- used for toggle-progress command
- font_size = 0, -- calculated in on_display_resize
- top_border = options.timeline_border,
- bottom_border = 0, -- set dynamically in `border` property observer
- init = function(this)
- -- Toggle 1px bottom border for timeline in no-border mode
- mp.observe_property('border', 'bool', function(_, border)
- this.bottom_border = not border and options.timeline_border or 0
- request_render()
- end)
- end,
- get_effective_proximity = function(this)
- if (elements.volume_slider and elements.volume_slider.pressed) then return 0 end
- if this.pressed then return 1 end
- return this.forced_proximity and this.forced_proximity or this.proximity
- end,
- get_effective_size_min = function(this)
- return this.size_min_override or this.size_min
- end,
- get_effective_size = function(this)
- if elements.speed and elements.speed.dragging then return this.size_max end
- local size_min = this:get_effective_size_min()
- return size_min + math.ceil((this.size_max - size_min) * this:get_effective_proximity())
- end,
- on_display_resize = function(this)
- if state.fullscreen or state.maximized then
- this.size_min = options.timeline_size_min_fullscreen
- this.size_max = options.timeline_size_max_fullscreen
- else
- this.size_min = options.timeline_size_min
- this.size_max = options.timeline_size_max
- end
- this.font_size = math.floor(math.min((this.size_max + 60) * 0.2, this.size_max * 0.96) * options.timeline_font_scale)
- this.ax = 0
- this.ay = display.height - this.size_max - this.top_border - this.bottom_border
- this.bx = display.width
- this.by = display.height
- end,
- set_from_cursor = function(this)
- mp.commandv('seek', ((cursor.x / display.width) * 100), 'absolute-percent+exact')
- end,
- on_mbtn_left_down = function(this)
- this.pressed = true
- this:set_from_cursor()
- end,
- on_global_mbtn_left_up = function(this) this.pressed = false end,
- on_global_mouse_leave = function(this) this.pressed = false end,
- on_global_mouse_move = function(this)
- if this.pressed then this:set_from_cursor() end
- end,
- on_wheel_up = function(this)
- if options.timeline_step > 0 then mp.commandv('seek', -options.timeline_step) end
- end,
- on_wheel_down = function(this)
- if options.timeline_step > 0 then mp.commandv('seek', options.timeline_step) end
- end,
- render = render_timeline,
-}))
-if options.top_bar_controls or options.top_bar_title then
- elements:add('top_bar', Element.new({
- button_opacity = 0.8,
- enabled = false,
- init = function(this)
- mp.observe_property('border', 'bool', function(_, border)
- this.enabled = not border
- end)
- end,
- get_effective_proximity = function(this)
- if (elements.volume_slider and elements.volume_slider.pressed) or elements.curtain.opacity > 0 then return 0 end
- return this.forced_proximity and this.forced_proximity or this.proximity
- end,
- on_display_resize = function(this)
- this.size = (state.fullscreen or state.maximized) and options.top_bar_size_fullscreen or options.top_bar_size
- this.icon_size = round(this.size / 8)
- this.spacing = math.ceil(this.size * 0.25)
- this.font_size = math.floor(this.size - (this.spacing * 2))
- this.button_width = round(this.size * 1.15)
- this.title_bx = display.width - (options.top_bar_controls and (this.button_width * 3) or 0)
- this.ax = options.top_bar_title and 0 or this.title_bx
- this.ay = 0
- this.bx = display.width
- this.by = this.size
- end,
- render = render_top_bar,
- }))
-end
-if options.top_bar_controls then
- elements:add('window_controls_minimize', Element.new({
- captures = {mouse_buttons = true},
- on_display_resize = function(this)
- this.ax = display.width - (elements.top_bar.button_width * 3)
- this.ay = 0
- this.bx = this.ax + elements.top_bar.button_width
- this.by = elements.top_bar.size
- end,
- on_mbtn_left_down = function() mp.commandv('cycle', 'window-minimized') end
- }))
- elements:add('window_controls_maximize', Element.new({
- captures = {mouse_buttons = true},
- on_display_resize = function(this)
- this.ax = display.width - (elements.top_bar.button_width * 2)
- this.ay = 0
- this.bx = this.ax + elements.top_bar.button_width
- this.by = elements.top_bar.size
- end,
- on_mbtn_left_down = function() mp.commandv('cycle', 'window-maximized') end
- }))
- elements:add('window_controls_close', Element.new({
- captures = {mouse_buttons = true},
- on_display_resize = function(this)
- this.ax = display.width - elements.top_bar.button_width
- this.ay = 0
- this.bx = this.ax + elements.top_bar.button_width
- this.by = elements.top_bar.size
- end,
- on_mbtn_left_down = function() mp.commandv('quit') end
- }))
-end
-if itable_find({'left', 'right'}, options.volume) then
- elements:add('volume', Element.new({
- width = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
- height = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
- margin = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
- get_effective_proximity = function(this)
- if elements.volume_slider.pressed then return 1 end
- if elements.timeline.proximity_raw == 0 or elements.curtain.opacity > 0 then return 0 end
- return this.forced_proximity and this.forced_proximity or this.proximity
- end,
- on_display_resize = function(this)
- this.width = (state.fullscreen or state.maximized) and options.volume_size_fullscreen or options.volume_size
- this.height = round(math.min(this.width * 8, (elements.timeline.ay - elements.top_bar.size) * 0.8))
- -- Don't bother rendering this if too small
- if this.height < (this.width * 2) then
- this.height = 0
- end
- this.margin = this.width / 2
- this.ax = round(options.volume == 'left' and this.margin or display.width - this.margin - this.width)
- this.ay = round((display.height - this.height) / 2)
- this.bx = round(this.ax + this.width)
- this.by = round(this.ay + this.height)
- end,
- render = render_volume,
- }))
- elements:add('volume_mute', Element.new({
- captures = {mouse_buttons = true},
- width = 0,
- height = 0,
- on_display_resize = function(this)
- this.width = elements.volume.width
- this.height = this.width
- this.ax = elements.volume.ax
- this.ay = elements.volume.by - this.height
- this.bx = elements.volume.bx
- this.by = elements.volume.by
- end,
- on_mbtn_left_down = function(this) mp.commandv('cycle', 'mute') end
- }))
- elements:add('volume_slider', Element.new({
- captures = {mouse_buttons = true, wheel = true},
- pressed = false,
- width = 0,
- height = 0,
- nudge_y = 0, -- vertical position where volume overflows 100
- nudge_size = nil, -- set on resize
- font_size = nil,
- spacing = nil,
- on_display_resize = function(this)
- this.ax = elements.volume.ax
- this.ay = elements.volume.ay
- this.bx = elements.volume.bx
- this.by = elements.volume_mute.ay
- this.width = this.bx - this.ax
- this.height = this.by - this.ay
- this.nudge_y = this.by - round(this.height * (100 / state.volume_max))
- this.nudge_size = round(elements.volume.width * 0.18)
- this.draw_nudge = this.ay < this.nudge_y
- this.spacing = round(this.width * 0.2)
- end,
- set_from_cursor = function(this)
- local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
- local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
- new_volume = round(new_volume / options.volume_step) * options.volume_step
- if state.volume ~= new_volume then mp.commandv('set', 'volume', math.min(new_volume, state.volume_max)) end
- end,
- on_mbtn_left_down = function(this)
- this.pressed = true
- this:set_from_cursor()
- end,
- on_global_mbtn_left_up = function(this) this.pressed = false end,
- on_global_mouse_leave = function(this) this.pressed = false end,
- on_global_mouse_move = function(this)
- if this.pressed then this:set_from_cursor() end
- end,
- on_wheel_up = function(this)
- local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
- mp.commandv('set', 'volume', math.min(current_rounded_volume + options.volume_step, state.volume_max))
- end,
- on_wheel_down = function(this)
- local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
- mp.commandv('set', 'volume', math.min(current_rounded_volume - options.volume_step, state.volume_max))
- end,
- }))
-end
-if options.speed then
- elements:add('speed', Element.new({
- captures = {mouse_buttons = true, wheel = true},
- dragging = nil,
- width = 0,
- height = 0,
- notches = 10,
- notch_every = 0.1,
- step_distance = nil,
- font_size = nil,
- init = function(this)
- -- Fade out/in on timeline mouse enter/leave
- elements.timeline:on('mouse_enter', function()
- if not this.dragging then this:fadeout() end
- end)
- elements.timeline:on('mouse_leave', function()
- if not this.dragging then this:fadein() end
- end)
- end,
- fadeout = function(this)
- this:tween_property('forced_proximity', 1, 0, function(this)
- this.forced_proximity = 0
- end)
- end,
- fadein = function(this)
- local get_current_proximity = function() return this.proximity end
- this:tween_property('forced_proximity', 0, get_current_proximity, function(this)
- this.forced_proximity = nil
- end)
- end,
- on_display_resize = function(this)
- this.height = (state.fullscreen or state.maximized) and options.speed_size_fullscreen or options.speed_size
- this.width = round(this.height * 3.6)
- this.notch_spacing = this.width / this.notches
- this.step_distance = this.notch_spacing * (options.speed_step / this.notch_every)
- this.ax = (display.width - this.width) / 2
- this.by = display.height - elements.timeline.size_max
- this.ay = this.by - this.height
- this.bx = this.ax + this.width
- this.font_size = round(this.height * 0.48 * options.speed_font_scale)
- end,
- set_from_cursor = function(this)
- local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
- local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
- new_volume = round(new_volume / options.volume_step) * options.volume_step
- if state.volume ~= new_volume then mp.commandv('set', 'volume', new_volume) end
- end,
- on_mbtn_left_down = function(this)
- this:tween_stop() -- Stop and cleanup possible ongoing animations
- this.dragging = {
- start_time = mp.get_time(),
- start_x = cursor.x,
- distance = 0,
- start_speed = state.speed
- }
- end,
- on_global_mouse_move = function(this)
- if not this.dragging then return end
-
- this.dragging.distance = cursor.x - this.dragging.start_x
- local steps_dragged = round(-this.dragging.distance / this.step_distance)
- local new_speed = this.dragging.start_speed + (steps_dragged * options.speed_step)
- mp.set_property_native('speed', round(new_speed * 100) / 100)
- end,
- on_mbtn_left_up = function(this)
- -- Reset speed on short clicks
- if this.dragging and math.abs(this.dragging.distance) < 6 and mp.get_time() - this.dragging.start_time < 0.15 then
- mp.set_property_native('speed', 1)
- end
- end,
- on_global_mbtn_left_up = function(this)
- if this.dragging and elements.timeline.proximity_raw == 0 then
- this:fadeout()
- end
- this.dragging = nil
- request_render()
- end,
- on_global_mouse_leave = function(this)
- this.dragging = nil
- request_render()
- end,
- on_wheel_up = function(this)
- mp.set_property_native('speed', state.speed - options.speed_step)
- end,
- on_wheel_down = function(this)
- mp.set_property_native('speed', state.speed + options.speed_step)
- end,
- render = render_speed,
- }))
-end
-elements:add('curtain', Element.new({
- opacity = 0,
- fadeout = function(this)
- this:tween_property('opacity', this.opacity, 0);
- end,
- fadein = function(this)
- this:tween_property('opacity', this.opacity, 1);
- end,
- render = function(this)
- if this.opacity > 0 then
- local ass = assdraw.ass_new()
- ass:new_event()
- ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
- ass:append(ass_opacity(0.4, this.opacity))
- ass:pos(0, 0)
- ass:draw_start()
- ass:rect_cw(0, 0, display.width, display.height)
- ass:draw_stop()
- return ass
- end
- end
-}))
-
--- CHAPTERS SERIALIZATION
-
--- Parse `chapter_ranges` option into workable data structure
-for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
- local start_patterns, color, opacity, end_patterns = string.match(definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)')
-
- -- Invalid definition
- if start_patterns == nil then goto continue end
-
- start_patterns = start_patterns:lower()
- end_patterns = end_patterns:lower()
- local uses_bof = start_patterns:find('{bof}') ~= nil
- local uses_eof = end_patterns:find('{eof}') ~= nil
- local chapter_range = {
- start_patterns = split(start_patterns, '|'),
- end_patterns = split(end_patterns, '|'),
- color = color,
- opacity = tonumber(opacity),
- ranges = {}
- }
-
- -- Filter out special keywords so we don't use them when matching titles
- if uses_bof then
- chapter_range.start_patterns = itable_remove(chapter_range.start_patterns, '{bof}')
- end
- if uses_eof and chapter_range.end_patterns then
- chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, '{eof}')
- end
-
- chapter_range['serialize'] = function (chapters)
- chapter_range.ranges = {}
- local current_range = nil
- -- bof and eof should be used only once per timeline
- -- eof is only used when last range is missing end
- local bof_used = false
-
- function start_range(chapter)
- -- If there is already a range started, should we append or overwrite?
- -- I chose overwrite here.
- current_range = {['start'] = chapter}
- end
-
- function end_range(chapter)
- current_range['end'] = chapter
- chapter_range.ranges[#chapter_range.ranges + 1] = current_range
- -- Mark both chapter objects
- current_range['start']._uosc_used_as_range_point = true
- current_range['end']._uosc_used_as_range_point = true
- -- Clear for next range
- current_range = nil
- end
-
- for _, chapter in ipairs(chapters) do
- if type(chapter.title) == 'string' then
- local lowercase_title = chapter.title:lower()
- local is_end = false
- local is_start = false
-
- -- Is ending check and handling
- if chapter_range.end_patterns then
- for _, end_pattern in ipairs(chapter_range.end_patterns) do
- is_end = is_end or lowercase_title:find(end_pattern) ~= nil
- end
-
- if is_end then
- if current_range == nil and uses_bof and not bof_used then
- bof_used = true
- start_range({time = 0})
- end
- if current_range ~= nil then
- end_range(chapter)
- else
- is_end = false
- end
- end
- end
-
- -- Is start check and handling
- for _, start_pattern in ipairs(chapter_range.start_patterns) do
- is_start = is_start or lowercase_title:find(start_pattern) ~= nil
- end
-
- if is_start then start_range(chapter) end
- end
- end
-
- -- If there is an unfinished range and range type accepts eof, use it
- if current_range ~= nil and uses_eof then
- end_range({time = state.duration or infinity})
- end
- end
-
- state.chapter_ranges = state.chapter_ranges or {}
- state.chapter_ranges[#state.chapter_ranges + 1] = chapter_range
-
- ::continue::
-end
-
-function parse_chapters()
- -- Sometimes state.duration is not initialized yet for some reason
- state.duration = mp.get_property_native('duration')
-
- local chapters = get_normalized_chapters()
-
- if not chapters or not state.duration then return end
-
- -- Reset custom ranges
- for _, chapter_range in ipairs(state.chapter_ranges or {}) do
- chapter_range.serialize(chapters)
- end
-
- -- Filter out chapters that were used as ranges
- state.chapters = itable_remove(chapters, function(chapter)
- return chapter._uosc_used_as_range_point == true
- end)
-
- request_render()
-end
-
--- CONTEXT MENU SERIALIZATION
-
-state.context_menu_items = (function()
- local input_conf_path = mp.command_native({'expand-path', '~~/input.conf'})
- local input_conf_meta, meta_error = utils.file_info(input_conf_path)
-
- -- File doesn't exist
- if not input_conf_meta or not input_conf_meta.is_file then return end
-
- local items = {}
- local items_by_command = {}
- local submenus_by_id = {}
-
- for line in io.lines(input_conf_path) do
- local key, command, title = string.match(line, ' *([%S]+) +(.*) #! *(.*)')
- if key then
- local is_dummy = key:sub(1, 1) == '#'
- local submenu_id = ''
- local target_menu = items
- local title_parts = split(title or '', ' *> *')
-
- for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do
- if index < #title_parts then
- submenu_id = submenu_id .. title_part
-
- if not submenus_by_id[submenu_id] then
- submenus_by_id[submenu_id] = {title = title_part, items = {}}
- target_menu[#target_menu + 1] = submenus_by_id[submenu_id]
- end
-
- target_menu = submenus_by_id[submenu_id].items
- else
- -- If command is already in menu, just append the key to it
- if items_by_command[command] then
- items_by_command[command].hint = items_by_command[command].hint..', '..key
- else
- items_by_command[command] = {
- title = title_part,
- hint = not is_dummy and key or nil,
- value = command
- }
- target_menu[#target_menu + 1] = items_by_command[command]
- end
- end
- end
- end
- end
-
- if #items > 0 then return items end
-end)()
-
--- EVENT HANDLERS
-
-function create_state_setter(name)
- return function(_, value)
- state[name] = value
- dispatch_event_to_elements('prop_'..name, value)
- request_render()
- end
-end
-
-function dispatch_event_to_elements(name, ...)
- for _, element in pairs(elements) do
- if element.proximity_raw == 0 then
- element:maybe('on_'..name, ...)
- end
- element:maybe('on_global_'..name, ...)
- end
-end
-
-function create_event_to_elements_dispatcher(name, ...)
- return function(...) dispatch_event_to_elements(name, ...) end
-end
-
-function handle_mouse_leave()
- -- Slowly fadeout elements that are currently visible
- for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
- local element = elements[element_name]
- if element and element.proximity > 0 then
- element:tween_property('forced_proximity', element:get_effective_proximity(), 0, function()
- element.forced_proximity = nil
- end)
- end
- end
-
- cursor.hidden = true
- update_proximities()
- dispatch_event_to_elements('mouse_leave')
-end
-
-function handle_mouse_enter()
- cursor.hidden = false
- cursor.x, cursor.y = mp.get_mouse_pos()
- tween_element_stop(state)
- dispatch_event_to_elements('mouse_enter')
-end
-
-function handle_mouse_move()
- -- Handle case when we are in cursor hidden state but not left the actual
- -- window (i.e. when autohide simulates mouse_leave).
- if cursor.hidden then
- handle_mouse_enter()
- return
- end
-
- cursor.x, cursor.y = mp.get_mouse_pos()
- update_proximities()
- dispatch_event_to_elements('mouse_move')
- request_render()
-
- -- Restart timer that hides UI when mouse is autohidden
- if options.autohide then
- state.cursor_autohide_timer:kill()
- state.cursor_autohide_timer:resume()
- end
-end
-
-function navigate_directory(direction)
- local path = mp.get_property_native("path")
-
- if not path or is_protocol(path) then return end
-
- local next_file = get_adjacent_file(path, direction, options.media_types)
-
- if next_file then
- mp.commandv("loadfile", utils.join_path(serialize_path(path).dirname, next_file))
- end
-end
-
-function load_file_in_current_directory(index)
- local path = mp.get_property_native("path")
-
- if not path or is_protocol(path) then return end
-
- local dirname = serialize_path(path).dirname
- local files = get_files_in_directory(dirname, options.media_types)
-
- if not files then return end
- if index < 0 then index = #files + index + 1 end
-
- if files[index] then
- mp.commandv("loadfile", utils.join_path(dirname, files[index]))
- end
-end
-
--- MENUS
-
-function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop)
- return function()
- if menu:is_open(track_type) then menu:close() return end
-
- local items = {}
- local active_item = nil
-
- for index, track in ipairs(mp.get_property_native('track-list')) do
- if track.type == track_type then
- if track.selected then active_item = track.id end
-
- items[#items + 1] = {
- title = (track.title and track.title or 'Track '..track.id),
- hint = track.lang and track.lang:upper() or nil,
- value = track.id
- }
- end
- end
-
- -- Add option to disable a subtitle track. This works for all tracks,
- -- but why would anyone want to disable audio or video? Better to not
- -- let people mistakenly select what is unwanted 99.999% of the time.
- -- If I'm mistaken and there is an active need for this, feel free to
- -- open an issue.
- if track_type == 'sub' then
- active_item = active_item and active_item + 1 or 1
- table.insert(items, 1, {hint = 'disabled', value = nil})
- end
-
- menu:open(items, function(id)
- mp.commandv('set', track_prop, id and id or 'no')
-
- -- If subtitle track was selected, assume user also wants to see it
- if id and track_type == 'sub' then
- mp.commandv('set', 'sub-visibility', 'yes')
- end
-
- menu:close()
- end, {type = track_type, title = menu_title, active_item = active_item})
- end
-end
-
--- `menu_options`:
--- **allowed_types** - table with file extensions to display
--- **active_path** - full path of a file to preselect
--- Rest of the options are passed to `menu:open()`
-function open_file_navigation_menu(directory, handle_select, menu_options)
- directory = serialize_path(directory)
- local directories, error = utils.readdir(directory.path, 'dirs')
- local files, error = get_files_in_directory(directory.path, menu_options.allowed_types)
-
- if not files or not directories then
- msg.error('Retrieving files from '..directory..' failed: '..(error or ''))
- return
- end
-
- -- Files are already sorted
- table.sort(directories, word_order_comparator)
-
- -- Pre-populate items with parent directory selector if not at root
- local items = not directory.dirname and {} or {
- {title = '..', hint = 'parent dir', value = directory.dirname}
- }
-
- for _, dir in ipairs(directories) do
- local serialized = serialize_path(utils.join_path(directory.path, dir))
- items[#items + 1] = {title = serialized.basename, value = serialized.path, hint = '/'}
- end
-
- menu_options.active_item = nil
-
- for _, file in ipairs(files) do
- local serialized = serialize_path(utils.join_path(directory.path, file))
- local item_index = #items + 1
-
- items[item_index] = {
- title = serialized.basename,
- value = serialized.path,
- }
-
- if menu_options.active_path == serialized.path then
- menu_options.active_item = item_index
- end
- end
-
- menu_options.title = directory.basename..'/'
-
- menu:open(items, function(path)
- local meta, error = utils.file_info(path)
-
- if not meta then
- msg.error('Retrieving file info for '..path..' failed: '..(error or ''))
- return
- end
-
- if meta.is_dir then
- open_file_navigation_menu(path, handle_select, menu_options)
- else
- handle_select(path)
- menu:close()
- end
- end, menu_options)
-end
-
--- VALUE SERIALIZATION/NORMALIZATION
-
-options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
-options.chapters = itable_find({'dots', 'lines', 'lines-top', 'lines-bottom'}, options.chapters) and options.chapters or 'none'
-options.media_types = split(options.media_types, ' *, *')
-options.subtitle_types = split(options.subtitle_types, ' *, *')
-options.timeline_cached_ranges = (function()
- if options.timeline_cached_ranges == '' or options.timeline_cached_ranges == 'no' then return nil end
- local parts = split(options.timeline_cached_ranges, ':')
- return parts[1] and {color = parts[1], opacity = tonumber(parts[2])} or nil
-end)()
-
--- HOOKS
-mp.register_event('file-loaded', parse_chapters)
-mp.observe_property('chapter-list', 'native', parse_chapters)
-mp.observe_property('duration', 'number', create_state_setter('duration'))
-mp.observe_property('media-title', 'string', create_state_setter('media_title'))
-mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen'))
-mp.observe_property('window-maximized', 'bool', create_state_setter('maximized'))
-mp.observe_property('idle-active', 'bool', create_state_setter('idle'))
-mp.observe_property('speed', 'number', create_state_setter('speed'))
-mp.observe_property('pause', 'bool', create_state_setter('pause'))
-mp.observe_property('volume', 'number', create_state_setter('volume'))
-mp.observe_property('volume-max', 'number', create_state_setter('volume_max'))
-mp.observe_property('mute', 'bool', create_state_setter('mute'))
-mp.observe_property('playback-time', 'number', function(name, val)
- -- Ignore the initial call with nil value
- if val == nil then return end
-
- state.position = val
- state.elapsed_seconds = val
- state.elapsed_time = state.elapsed_seconds and mp.format_time(state.elapsed_seconds) or nil
- state.remaining_seconds = mp.get_property_native('playtime-remaining')
- state.remaining_time = state.remaining_seconds and mp.format_time(state.remaining_seconds) or nil
-
- request_render()
-end)
-mp.observe_property('osd-dimensions', 'native', function(name, val)
- update_display_dimensions()
- request_render()
-end)
-mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
- if cache_state == nil then
- state.cached_ranges = nil
- return
- end
- local cache_ranges = cache_state['seekable-ranges']
- state.cached_ranges = #cache_ranges > 0 and cache_ranges or nil
-end)
-
--- CONTROLS
-
--- Mouse movement key binds
-local base_keybinds = {
- {'mouse_move', handle_mouse_move},
- {'mouse_leave', handle_mouse_leave},
- {'mouse_enter', handle_mouse_enter},
-}
-if options.pause_on_click_shorter_than > 0 then
- -- Cycles pause when click is shorter than `options.pause_on_click_shorter_than`
- -- while filtering out double clicks.
- local duration_seconds = options.pause_on_click_shorter_than / 1000
- local last_down_event;
- local click_timer = mp.add_timeout(duration_seconds, function()
- mp.command('cycle pause')
- end);
- click_timer:kill()
- base_keybinds[#base_keybinds + 1] = {'mbtn_left', function()
- if mp.get_time() - last_down_event < duration_seconds then
- click_timer:resume()
- end
- end, function()
- if click_timer:is_enabled() then
- click_timer:kill()
- last_down_event = 0
- else
- last_down_event = mp.get_time()
- end
- end
- }
-end
-mp.set_key_bindings(base_keybinds, 'mouse_movement', 'force')
-mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor')
-
--- Context based key bind groups
-
-forced_key_bindings = (function()
- mp.set_key_bindings({
- {'mbtn_left', create_event_to_elements_dispatcher('mbtn_left_up'), create_event_to_elements_dispatcher('mbtn_left_down')},
- {'mbtn_left_dbl', 'ignore'},
- }, 'mouse_buttons', 'force')
- mp.set_key_bindings({
- {'wheel_up', create_event_to_elements_dispatcher('wheel_up')},
- {'wheel_down', create_event_to_elements_dispatcher('wheel_down')},
- }, 'wheel', 'force')
-
- local groups = {}
- for _, group in ipairs({'mouse_buttons', 'wheel'}) do
- groups[group] = {
- is_enabled = false,
- enable = function(this)
- if this.is_enabled then return end
- this.is_enabled = true
- mp.enable_key_bindings(group)
- end,
- disable = function(this)
- if not this.is_enabled then return end
- this.is_enabled = false
- mp.disable_key_bindings(group)
- end,
- }
- end
- return groups
-end)()
-
--- KEY BINDABLE FEATURES
-
-mp.add_key_binding(nil, 'peek-timeline', function()
- if elements.timeline.proximity > 0.5 then
- elements.timeline:tween_property('proximity', elements.timeline.proximity, 0)
- else
- elements.timeline:tween_property('proximity', elements.timeline.proximity, 1)
- end
-end)
-mp.add_key_binding(nil, 'toggle-progress', function()
- local timeline = elements.timeline
- if timeline.size_min_override then
- timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function()
- timeline.size_min_override = nil
- end)
- else
- timeline:tween_property('size_min_override', timeline.size_min, 0)
- end
-end)
-mp.add_key_binding(nil, 'flash-timeline', function()
- elements.timeline:flash()
-end)
-mp.add_key_binding(nil, 'flash-volume', function()
- if elements.volume then elements.volume:flash() end
-end)
-mp.add_key_binding(nil, 'flash-speed', function()
- if elements.speed then elements.speed:flash() end
-end)
-mp.add_key_binding(nil, 'menu', function()
- if menu:is_open('menu') then
- menu:close()
- elseif state.context_menu_items then
- menu:open(state.context_menu_items, function(command)
- mp.command(command)
- end, {type = 'menu'})
- end
-end)
-mp.add_key_binding(nil, 'load-subtitles', function()
- if menu:is_open('load-subtitles') then menu:close() return end
-
- local path = mp.get_property_native('path')
- if path and not is_protocol(path) then
- open_file_navigation_menu(
- serialize_path(path).dirname,
- function(path) mp.commandv('sub-add', path) end,
- {
- type = 'load-subtitles',
- allowed_types = options.subtitle_types
- }
- )
- end
-end)
-mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener('Subtitles', 'sub', 'sid'))
-mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener('Audio', 'audio', 'aid'))
-mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener('Video', 'video', 'vid'))
-mp.add_key_binding(nil, 'playlist', function()
- if menu:is_open('playlist') then menu:close() return end
-
- function serialize_playlist()
- local pos = mp.get_property_number('playlist-pos-1', 0)
- local items = {}
- local active_item
- for index, item in ipairs(mp.get_property_native('playlist')) do
- local is_url = item.filename:find('://')
- items[index] = {
- title = is_url and item.filename or serialize_path(item.filename).basename,
- hint = tostring(index),
- value = index
- }
-
- if index == pos then active_item = index end
- end
- return items, active_item
- end
-
- -- Update active index and playlist content on playlist changes
- function handle_playlist_change()
- if menu:is_open('playlist') then
- local items, active_item = serialize_playlist()
- elements.menu:set_items(items, {
- active_item = active_item,
- selected_item = active_item
- })
- end
- end
-
- local items, active_item = serialize_playlist()
-
- menu:open(items, function(index)
- mp.commandv('set', 'playlist-pos-1', tostring(index))
- end, {
- type = 'playlist',
- title = 'Playlist',
- active_item = active_item,
- on_open = function()
- mp.observe_property('playlist', 'native', handle_playlist_change)
- mp.observe_property('playlist-pos-1', 'native', handle_playlist_change)
- end,
- on_close = function()
- mp.unobserve_property(handle_playlist_change)
- end,
- })
-end)
-mp.add_key_binding(nil, 'chapters', function()
- if menu:is_open('chapters') then menu:close() return end
-
- local items = {}
- local chapters = get_normalized_chapters()
-
- for index, chapter in ipairs(chapters) do
- items[#items + 1] = {
- title = chapter.title or '',
- hint = mp.format_time(chapter.time),
- value = chapter.time
- }
- end
-
- -- Select first chapter from the end with time lower
- -- than current playing position (with 100ms leeway).
- function get_selected_chapter_index()
- local position = mp.get_property_native('playback-time')
- if not position then return nil end
- for index = #items, 1, -1 do
- if position - 0.1 > items[index].value then return index end
- end
- end
-
- -- Update selected chapter in chapter navigation menu
- function seek_handler()
- if menu:is_open('chapters') then
- elements.menu:activate_index(get_selected_chapter_index())
- end
- end
-
- menu:open(items, function(time)
- mp.commandv('seek', tostring(time), 'absolute')
- end, {
- type = 'chapters',
- title = 'Chapters',
- active_item = get_selected_chapter_index(),
- on_open = function() mp.register_event('seek', seek_handler) end,
- on_close = function() mp.unregister_event(seek_handler) end
- })
-end)
-mp.add_key_binding(nil, 'show-in-directory', function()
- local path = mp.get_property_native('path')
-
- -- Ignore URLs
- if not path or is_protocol(path) then return end
-
- path = normalize_path(path)
-
- if state.os == 'windows' then
- utils.subprocess_detached({args = {'explorer', '/select,', path}, cancellable = false})
- elseif state.os == 'macos' then
- utils.subprocess_detached({args = {'open', '-R', path}, cancellable = false})
- elseif state.os == 'linux' then
- local result = utils.subprocess({args = {'nautilus', path}, cancellable = false})
-
- -- Fallback opens the folder with xdg-open instead
- if result.status ~= 0 then
- utils.subprocess({args = {'xdg-open', serialize_path(path).dirname}, cancellable = false})
- end
- end
-end)
-mp.add_key_binding(nil, 'open-file', function()
- if menu:is_open('open-file') then menu:close() return end
-
- local path = mp.get_property_native('path')
- local directory
- local active_file
-
- if path == nil or is_protocol(path) then
- local path = serialize_path(mp.command_native({'expand-path', '~/'}))
- directory = path.path
- active_file = nil
- else
- local path = serialize_path(path)
- directory = path.dirname
- active_file = path.path
- end
-
- -- Update selected file in directory navigation menu
- function handle_file_loaded()
- if menu:is_open('open-file') then
- local path = normalize_path(mp.get_property_native('path'))
- elements.menu:activate_value(path)
- elements.menu:select_value(path)
- end
- end
-
- open_file_navigation_menu(
- directory,
- function(path) mp.commandv('loadfile', path) end,
- {
- type = 'open-file',
- allowed_types = options.media_types,
- active_path = active_file,
- on_open = function() mp.register_event('file-loaded', handle_file_loaded) end,
- on_close = function() mp.unregister_event(handle_file_loaded) end,
- }
- )
-end)
-mp.add_key_binding(nil, 'next', function()
- if mp.get_property_native('playlist-count') > 1 then
- mp.command('playlist-next')
- else
- navigate_directory('forward')
- end
-end)
-mp.add_key_binding(nil, 'prev', function()
- if mp.get_property_native('playlist-count') > 1 then
- mp.command('playlist-prev')
- else
- navigate_directory('backward')
- end
-end)
-mp.add_key_binding(nil, 'next-file', function() navigate_directory('forward') end)
-mp.add_key_binding(nil, 'prev-file', function() navigate_directory('backward') end)
-mp.add_key_binding(nil, 'first', function()
- if mp.get_property_native('playlist-count') > 1 then
- mp.commandv('set', 'playlist-pos-1', '1')
- else
- load_file_in_current_directory(1)
- end
-end)
-mp.add_key_binding(nil, 'last', function()
- local playlist_count = mp.get_property_native('playlist-count')
- if playlist_count > 1 then
- mp.commandv('set', 'playlist-pos-1', tostring(playlist_count))
- else
- load_file_in_current_directory(-1)
- end
-end)
-mp.add_key_binding(nil, 'first-file', function() load_file_in_current_directory(1) end)
-mp.add_key_binding(nil, 'last-file', function() load_file_in_current_directory(-1) end)
-mp.add_key_binding(nil, 'delete-file-next', function()
- local path = mp.get_property_native('path')
-
- if not path or is_protocol(path) then return end
-
- path = normalize_path(path)
- local playlist_count = mp.get_property_native('playlist-count')
-
- if playlist_count > 1 then
- mp.commandv('playlist-remove', 'current')
- else
- local next_file = get_adjacent_file(path, 'forward', options.media_types)
-
- if menu:is_open('open-file') then
- elements.menu:delete_value(path)
- end
-
- if next_file then
- mp.commandv('loadfile', next_file)
- else
- mp.commandv('stop')
- end
- end
-
- os.remove(path)
-end)
-mp.add_key_binding(nil, 'delete-file-quit', function()
- local path = mp.get_property_native('path')
- if not path or is_protocol(path) then return end
- os.remove(normalize_path(path))
- mp.command('quit')
-end)
-mp.add_key_binding(nil, 'open-config-directory', function()
- local config = serialize_path(mp.command_native({'expand-path', '~~/mpv.conf'}))
- local args
-
- if state.os == 'windows' then
- args = {'explorer', '/select,', config.path}
- elseif state.os == 'macos' then
- args = {'open', '-R', config.path}
- elseif state.os == 'linux' then
- args = {'xdg-open', config.dirname}
- end
-
- utils.subprocess_detached({args = args, cancellable = false})
-end)