From 0a334f0c0dc98f3eb654f9291f9b3e7b0c548c29 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sun, 6 Feb 2022 22:32:14 -0800 Subject: updated mpv --- mpv/fonts/Material-Design-Iconic-Font.eot | Bin 0 -> 42495 bytes mpv/fonts/Material-Design-Iconic-Font.svg | 787 ++++++ mpv/fonts/Material-Design-Iconic-Font.ttf | Bin 0 -> 99212 bytes mpv/fonts/Material-Design-Iconic-Font.woff | Bin 0 -> 50312 bytes mpv/fonts/Material-Design-Iconic-Font.woff2 | Bin 0 -> 38384 bytes mpv/mpv.conf | 14 +- mpv/script_modules/mpvSockets/LICENSE | 21 + mpv/script_modules/mpvSockets/README.md | 76 + mpv/script_modules/mpvSockets/mpvSockets.lua | 36 + mpv/scripts/autosub.lua | 254 -- mpv/scripts/autosubsync.lua | 44 - mpv/scripts/modules.lua | 3 + mpv/scripts/morden.lua | 2041 ++++++++++++++ mpv/scripts/uosc.lua | 3230 ---------------------- mpv/watch_later/99533EEF7D7C98388A098612D29CE95A | 5 + 15 files changed, 2982 insertions(+), 3529 deletions(-) create mode 100644 mpv/fonts/Material-Design-Iconic-Font.eot create mode 100644 mpv/fonts/Material-Design-Iconic-Font.svg create mode 100644 mpv/fonts/Material-Design-Iconic-Font.ttf create mode 100644 mpv/fonts/Material-Design-Iconic-Font.woff create mode 100644 mpv/fonts/Material-Design-Iconic-Font.woff2 create mode 100644 mpv/script_modules/mpvSockets/LICENSE create mode 100644 mpv/script_modules/mpvSockets/README.md create mode 100644 mpv/script_modules/mpvSockets/mpvSockets.lua delete mode 100644 mpv/scripts/autosub.lua delete mode 100644 mpv/scripts/autosubsync.lua create mode 100644 mpv/scripts/modules.lua create mode 100644 mpv/scripts/morden.lua delete mode 100644 mpv/scripts/uosc.lua create mode 100644 mpv/watch_later/99533EEF7D7C98388A098612D29CE95A diff --git a/mpv/fonts/Material-Design-Iconic-Font.eot b/mpv/fonts/Material-Design-Iconic-Font.eot new file mode 100644 index 0000000..5e25191 Binary files /dev/null and b/mpv/fonts/Material-Design-Iconic-Font.eot differ diff --git a/mpv/fonts/Material-Design-Iconic-Font.svg b/mpv/fonts/Material-Design-Iconic-Font.svg new file mode 100644 index 0000000..1d3d2ea --- /dev/null +++ b/mpv/fonts/Material-Design-Iconic-Font.svg @@ -0,0 +1,787 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mpv/fonts/Material-Design-Iconic-Font.ttf b/mpv/fonts/Material-Design-Iconic-Font.ttf new file mode 100644 index 0000000..5d489fd Binary files /dev/null and b/mpv/fonts/Material-Design-Iconic-Font.ttf differ diff --git a/mpv/fonts/Material-Design-Iconic-Font.woff b/mpv/fonts/Material-Design-Iconic-Font.woff new file mode 100644 index 0000000..933b2bf Binary files /dev/null and b/mpv/fonts/Material-Design-Iconic-Font.woff differ diff --git a/mpv/fonts/Material-Design-Iconic-Font.woff2 b/mpv/fonts/Material-Design-Iconic-Font.woff2 new file mode 100644 index 0000000..35970e2 Binary files /dev/null and b/mpv/fonts/Material-Design-Iconic-Font.woff2 differ diff --git a/mpv/mpv.conf b/mpv/mpv.conf index 8f5edbd..286a922 100644 --- a/mpv/mpv.conf +++ b/mpv/mpv.conf @@ -1,6 +1,6 @@ # High Quality profile=gpu-hq -#hwdec=auto +hwdec=auto scale=ewa_lanczossharp dscale=mitchell cscale=spline36 @@ -14,6 +14,14 @@ volume=70 slang=eng,en,en-US,enUS blend-subtitles=yes +# disable osc +osc=no + +[idle] +profile-cond=p["idle-active"] +profile-restore=copy-equal +background=1 + # Play Finnish audio if available, fall back to English otherwise. alang=jpn,ja,jp,en @@ -34,3 +42,7 @@ screenshot-png-compression=1 screenshot-jpeg-quality=95 #Output directory screenshot-directory="~/pics/screenshots" + +# use yt-dlp +script-opts=ytdl_hook-ytdl_path=/usr/bin/yt-dlp +ytdl-format=bestvideo[height<=?1080]+bestaudio/best diff --git a/mpv/script_modules/mpvSockets/LICENSE b/mpv/script_modules/mpvSockets/LICENSE new file mode 100644 index 0000000..c5551bb --- /dev/null +++ b/mpv/script_modules/mpvSockets/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Wis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mpv/script_modules/mpvSockets/README.md b/mpv/script_modules/mpvSockets/README.md new file mode 100644 index 0000000..9d3e1fb --- /dev/null +++ b/mpv/script_modules/mpvSockets/README.md @@ -0,0 +1,76 @@ +# mpvSockets +create one sockets per mpv instance (with the instance's process **ID** (PID), (**unique**)), instead of one socket for the last started instance + +dangling sockets for crashed or killed instances is an issue, +not sure if this script should handle/remove them or the clients/users, or both. + +# Installation +Download the single script file to your mpv-scripts-directory +## Linux / unixes: +``` bash +curl "https://raw.githubusercontent.com/wis/mpvSockets/master/mpvSockets.lua" --create-dirs -o "$Your_Mpv_Scripts_Directory_Location/mpvSockets.lua" +``` +if you're on Linux, most likely the location is `~/.config/mpv/scripts`, so run this before: +``` bash +$Your_Mpv_Scripts_Directory_Location=$HOME/config/mpv/scripts +``` +## Windows (untested) +powershell: +``` powershell +Invoke-WebRequest -OutFile "$env:LOCALAPPDATA\mpv\scripts\mpvSockets.lua" "https://raw.githubusercontent.com/wis/mpvSockets/master/mpvSockets.lua" +``` + +# Usage, with Mpv's [JSON IPC](https://github.com/mpv-player/mpv/blob/master/DOCS/man/ipc.rst) +## Linux / unixes (unix sockets): +a script that pauses all running mpv instances: +bash: +``` bash +#!/bin/bash +for i in $(ls /tmp/mpvSockets/*); do + echo '{ "command": ["set_property", "pause", true] }' | socat - "$i"; +done +# Socat is a command line based utility that establishes two bidirec-tional byte streams and transfers data between them. +# available on Linux and FreeBSD, propably most unixes. you can also use +``` + +## Windows (named pipes): +quote from https://mpv.io/manual/stable/#command-prompt-example +> Unfortunately, it's not as easy to test the IPC protocol on Windows, since Windows ports of socat (in Cygwin and MSYS2) don't understand named pipes. In the absence of a simple tool to send and receive from bidirectional pipes, the echo command can be used to send commands, but not receive replies from the command prompt. +> +> Assuming mpv was started with: +> +> `mpv file.mkv --input-ipc-server=\\.\pipe\mpvsocket` +> You can send commands from a command prompt: +> +> `echo show-text ${playback-time} >\\.\pipe\mpvsocket` +To be able to simultaneously read and write from the IPC pipe, like on Linux, it's necessary to write an external program that uses overlapped file I/O (or some wrapper like .NET's NamedPipeClientStream.) + +powershell client writer and reader (untested): +``` powershell +# socat.ps1 +# usage: socat.ps1 +$sockedName = args[0] +$message = args[1] + +$npipeClient = new-object System.IO.Pipes.NamedPipeClientStream('.', $socketName, [System.IO.Pipes.PipeDirection]::InOut, [System.IO.Pipes.PipeOptions]::None, [System.Security.Principal.TokenImpersonationLevel]::Impersonation) + +$pipeReader = $pipeWriter = $null +try { + $npipeClient.Connect() + $pipeReader = new-object System.IO.StreamReader($npipeClient) + $pipeWriter = new-object System.IO.StreamWriter($npipeClient) + $pipeWriter.AutoFlush = $true + + $pipeWriter.WriteLine($message) + + while (($data = $pipeReader.ReadLine()) -ne $null) { + $data + } +} +catch { + "An error occurred that could not be resolved." +} +finally { + $npipeClient.Dispose() +} +``` \ No newline at end of file diff --git a/mpv/script_modules/mpvSockets/mpvSockets.lua b/mpv/script_modules/mpvSockets/mpvSockets.lua new file mode 100644 index 0000000..df8d078 --- /dev/null +++ b/mpv/script_modules/mpvSockets/mpvSockets.lua @@ -0,0 +1,36 @@ +-- mpvSockets, one socket per instance, removes socket on exit + +local utils = require 'mp.utils' + +local function get_temp_path() + local directory_seperator = package.config:match("([^\n]*)\n?") + local example_temp_file_path = os.tmpname() + + -- remove generated temp file + pcall(os.remove, example_temp_file_path) + + local seperator_idx = example_temp_file_path:reverse():find(directory_seperator) + local temp_path_length = #example_temp_file_path - seperator_idx + + return example_temp_file_path:sub(1, temp_path_length) +end + +tempDir = get_temp_path() + +function join_paths(...) + local arg={...} + path = "" + for i,v in ipairs(arg) do + path = utils.join_path(path, tostring(v)) + end + return path; +end + +ppid = utils.getpid() +os.execute("mkdir " .. join_paths(tempDir, "mpvSockets") .. " 2>/dev/null") +mp.set_property("options/input-ipc-server", join_paths(tempDir, "mpvSockets", ppid)) + +function shutdown_handler() + os.remove(join_paths(tempDir, "mpvSockets", ppid)) +end +mp.register_event("shutdown", shutdown_handler) 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 by 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 , 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_patternend_pattern -# ``` -# -# Multiple start and end patterns can be defined by separating them with `|`: -# ``` -# p1|pNp1|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) diff --git a/mpv/watch_later/99533EEF7D7C98388A098612D29CE95A b/mpv/watch_later/99533EEF7D7C98388A098612D29CE95A new file mode 100644 index 0000000..47706c9 --- /dev/null +++ b/mpv/watch_later/99533EEF7D7C98388A098612D29CE95A @@ -0,0 +1,5 @@ +start=827.473000 +volume=100.000000 +fullscreen=yes +aid=1 +sid=no -- cgit v1.2.3-13-gbd6f