diff options
author | Mole Shang <[email protected]> | 2023-03-11 07:47:09 +0800 |
---|---|---|
committer | Mole Shang <[email protected]> | 2023-03-11 07:47:09 +0800 |
commit | 2a4c3b70832316362a37a5e8734563cbfbc0de04 (patch) | |
tree | bf5cfad38b0e05a4ba4920e01981d7366947c673 /config | |
parent | d2a568a678f273dcb279b053653a636bf59a7d4c (diff) | |
download | dotfiles-2a4c3b70832316362a37a5e8734563cbfbc0de04.tar.gz dotfiles-2a4c3b70832316362a37a5e8734563cbfbc0de04.tar.bz2 dotfiles-2a4c3b70832316362a37a5e8734563cbfbc0de04.zip |
config/implay: new, 20230311
Diffstat (limited to 'config')
-rw-r--r-- | config/implay/fonts/Material-Design-Iconic-Font.ttf | bin | 0 -> 99212 bytes | |||
-rw-r--r-- | config/implay/implay.conf | 45 | ||||
-rw-r--r-- | config/implay/input.conf | 175 | ||||
-rw-r--r-- | config/implay/mpv.conf | 141 | ||||
-rw-r--r-- | config/implay/script-opts/modernf.conf | 68 | ||||
-rw-r--r-- | config/implay/script-opts/osc.conf | 3 | ||||
-rw-r--r-- | config/implay/scripts/modernf.lua | 2320 |
7 files changed, 2752 insertions, 0 deletions
diff --git a/config/implay/fonts/Material-Design-Iconic-Font.ttf b/config/implay/fonts/Material-Design-Iconic-Font.ttf Binary files differnew file mode 100644 index 0000000..5d489fd --- /dev/null +++ b/config/implay/fonts/Material-Design-Iconic-Font.ttf diff --git a/config/implay/implay.conf b/config/implay/implay.conf new file mode 100644 index 0000000..0853765 --- /dev/null +++ b/config/implay/implay.conf @@ -0,0 +1,45 @@ +[debug] +log-level=info +log-limit=500 + +[font] +glyph-range=1 +path=/usr/share/fonts/truetype/wqy/wqy-microhei.ttc +size=12 + +[interface] +docking=true +fps=60 +lang=zh-Hans +scale=2 +theme=dark +viewports=true + +[mpv] +config=false +volume=100 +watch-later=true +wid=false + +[recent] +file-0=[Airota&Nekomoe kissaten][Yuru Camp Season 2][08][BDRip 1080p AVC AAC][CHS].mp4 +file-1=[Airota&Nekomoe kissaten][Yuru Camp Season 2][07][BDRip 1080p AVC AAC][CHS].mp4 +file-2=[Airota&Nekomoe kissaten][Yuru Camp Season 2][06][BDRip 1080p AVC AAC][CHS].mp4 +file-3=[Airota&Nekomoe kissaten][Yuru Camp Season 2][05][BDRip 1080p AVC AAC][CHS].mp4 +file-4=[Airota&Nekomoe kissaten][Yuru Camp Season 2][04][BDRip 1080p AVC AAC][CHS].mp4 +file-5=[Airota&Nekomoe kissaten][Yuru Camp Season 2][03][BDRip 1080p AVC AAC][CHS].mp4 +file-6=[Airota&Nekomoe kissaten][Yuru Camp Season 2][02][BDRip 1080p AVC AAC][CHS].mp4 +file-7=[Airota&Nekomoe kissaten][Yuru Camp Season 2][01][BDRip 1080p AVC AAC][CHS].mp4 +file-8=[VCB-Studio] Sora yori mo Tooi Basho [09][Ma10p_1080p][x265_flac].mkv +file-9=[VCB-Studio] Sora yori mo Tooi Basho [08][Ma10p_1080p][x265_flac_aac].mkv +limit=10 +space-to-play-last=false + +[window] +h=1200 +save=true +single=false +w=1920 +x=0 +y=0 + diff --git a/config/implay/input.conf b/config/implay/input.conf new file mode 100644 index 0000000..37cdbf0 --- /dev/null +++ b/config/implay/input.conf @@ -0,0 +1,175 @@ +# mpv keybindings +# +# Lines starting with # are comments. Use SHARP to assign the # key. +# Copy this file and uncomment and edit the bindings you want to change. +# +# List of commands and further details: DOCS/man/input.rst +# List of special keys: --input-keylist +# Keybindings testing mode: mpv --input-test --force-window --idle +# +# Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore'). +# +# Strings need to be quoted and escaped: +# KEY show-text "This is a single backslash: \\ and a quote: \" !" +# +# You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with +# the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal). +# +# The default keybindings are hardcoded into the mpv binary. +# You can disable them completely with: --no-input-default-bindings + +# If this is enabled, treat all the following bindings as default. +#default-bindings start + +#MBTN_LEFT ignore # don't do anything +#MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen +#MBTN_RIGHT cycle pause # toggle pause/playback mode +#MBTN_BACK playlist-prev # skip to the previous file +#MBTN_FORWARD playlist-next # skip to the next file + +# Mouse wheels, touchpad or other input devices that have axes +# if the input devices supports precise scrolling it will also scale the +# numeric value accordingly +#WHEEL_UP seek 10 # seek 10 seconds forward +#WHEEL_DOWN seek -10 # seek 10 seconds backward +#WHEEL_LEFT add volume -2 +#WHEEL_RIGHT add volume 2 + +## Seek units are in seconds, but note that these are limited by keyframes +#RIGHT seek 5 # seek 5 seconds forward +#LEFT seek -5 # seek 5 seconds backward +#UP seek 60 # seek 1 minute forward +#DOWN seek -60 # seek 1 minute backward +# Do smaller, always exact (non-keyframe-limited), seeks with shift. +# Don't show them on the OSD (no-osd). +#Shift+RIGHT no-osd seek 1 exact # seek exactly 1 second forward +#Shift+LEFT no-osd seek -1 exact # seek exactly 1 second backward +#Shift+UP no-osd seek 5 exact # seek exactly 5 seconds forward +#Shift+DOWN no-osd seek -5 exact # seek exactly 5 seconds backward +#Ctrl+LEFT no-osd sub-seek -1 # seek to the previous subtitle +#Ctrl+RIGHT no-osd sub-seek 1 # seek to the next subtitle +#Ctrl+Shift+LEFT sub-step -1 # change subtitle timing such that the previous subtitle is displayed +#Ctrl+Shift+RIGHT sub-step 1 # change subtitle timing such that the next subtitle is displayed +#Alt+left add video-pan-x 0.1 # move the video right +#Alt+right add video-pan-x -0.1 # move the video left +#Alt+up add video-pan-y 0.1 # move the video down +#Alt+down add video-pan-y -0.1 # move the video up +#Alt++ add video-zoom 0.1 # zoom in +#Alt+- add video-zoom -0.1 # zoom out +#Alt+BS set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0 # reset zoom and pan settings +#PGUP add chapter 1 # seek to the next chapter +#PGDWN add chapter -1 # seek to the previous chapter +#Shift+PGUP seek 600 # seek 10 minutes forward +#Shift+PGDWN seek -600 # seek 10 minutes backward +#[ multiply speed 1/1.1 # decrease the playback speed +#] multiply speed 1.1 # increase the playback speed +#{ multiply speed 0.5 # halve the playback speed +#} multiply speed 2.0 # double the playback speed +#BS set speed 1.0 # reset the speed to normal +#Shift+BS revert-seek # undo the previous (or marked) seek +#Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek +#q quit +#Q quit-watch-later # exit and remember the playback position +#q {encode} quit 4 +#ESC set fullscreen no # leave fullscreen +#ESC {encode} quit 4 +#p cycle pause # toggle pause/playback mode +#. frame-step # advance one frame and pause +#, frame-back-step # go back by one frame and pause +#SPACE cycle pause # toggle pause/playback mode +#> playlist-next # skip to the next file +#ENTER playlist-next # skip to the next file +#< playlist-prev # skip to the previous file +#O no-osd cycle-values osd-level 3 1 # toggle displaying the OSD on user interaction or always +#o show-progress # show playback progress +#P show-progress # show playback progress +#i script-binding stats/display-stats # display information and statistics +#I script-binding stats/display-stats-toggle # toggle displaying information and statistics +#` script-binding console/enable # open the console +#z add sub-delay -0.1 # shift subtitles 100 ms earlier +#Z add sub-delay +0.1 # delay subtitles by 100 ms +#x add sub-delay +0.1 # delay subtitles by 100 ms +#ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio +#ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier +#Shift+g add sub-scale +0.1 # increase the subtitle font size +#Shift+f add sub-scale -0.1 # decrease the subtitle font size +#9 add volume -2 +#/ add volume -2 +#0 add volume 2 +#* add volume 2 +#m cycle mute # toggle mute +#1 add contrast -1 +#2 add contrast 1 +#3 add brightness -1 +#4 add brightness 1 +#5 add gamma -1 +#6 add gamma 1 +#7 add saturation -1 +#8 add saturation 1 +#Alt+0 set current-window-scale 0.5 # halve the window size +#Alt+1 set current-window-scale 1.0 # reset the window size +#Alt+2 set current-window-scale 2.0 # double the window size +#d cycle deinterlace # toggle the deinterlacing filter +#r add sub-pos -1 # move subtitles up +#R add sub-pos +1 # move subtitles down +#t add sub-pos +1 # move subtitles down +#v cycle sub-visibility # hide or show the subtitles +#Alt+v cycle secondary-sub-visibility # hide or show the secondary subtitles +#V cycle sub-ass-vsfilter-aspect-compat # toggle stretching SSA/ASS subtitles with anamorphic videos to match the historical renderer +#u cycle-values sub-ass-override "force" "no" # toggle overriding SSA/ASS subtitle styles with the normal styles +#j cycle sub # switch subtitle track +#J cycle sub down # switch subtitle track backwards +#SHARP cycle audio # switch audio track +#_ cycle video # switch video track +#T cycle ontop # toggle placing the video on top of other windows +#f cycle fullscreen # toggle fullscreen +#s screenshot # take a screenshot of the video in its original resolution with subtitles +#S screenshot video # take a screenshot of the video in its original resolution without subtitles +#Ctrl+s screenshot window # take a screenshot of the window with OSD and subtitles +#Alt+s screenshot each-frame # automatically screenshot every frame; issue this command again to stop taking screenshots +#w add panscan -0.1 # decrease panscan +#W add panscan +0.1 # shrink black bars by cropping the video +#e add panscan +0.1 # shrink black bars by cropping the video +#A cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect) +#POWER quit +#PLAY cycle pause # toggle pause/playback mode +#PAUSE cycle pause # toggle pause/playback mode +#PLAYPAUSE cycle pause # toggle pause/playback mode +#PLAYONLY set pause no # unpause +#PAUSEONLY set pause yes # pause +#STOP quit +#FORWARD seek 60 # seek 1 minute forward +#REWIND seek -60 # seek 1 minute backward +#NEXT playlist-next # skip to the next file +#PREV playlist-prev # skip to the previous file +#VOLUME_UP add volume 2 +#VOLUME_DOWN add volume -2 +#MUTE cycle mute # toggle mute +#CLOSE_WIN quit +#CLOSE_WIN {encode} quit 4 +#ctrl+w quit +#E cycle edition # switch edition +#l ab-loop # set/clear A-B loop points +#L cycle-values loop-file "inf" "no" # toggle infinite looping +#ctrl+c quit 4 +#DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always +#ctrl+h cycle-values hwdec "auto" "no" # toggle hardware decoding +#F8 show-text ${playlist} # show the playlist +#F9 show-text ${track-list} # show the list of video, audio and sub tracks + +# +# Legacy bindings (may or may not be removed in the future) +# +#! add chapter -1 # seek to the previous chapter +#@ add chapter 1 # seek to the next chapter + +# +# Not assigned by default +# (not an exhaustive list of unbound commands) +# + +# ? cycle sub-forced-only # toggle DVD forced subs +# ? stop # stop playback (quit or enter idle mode) + +MBTN_RIGHT script-message-to implay context-menu # show context menu +Ctrl+Shift+p script-message-to implay command-palette # show command palette diff --git a/config/implay/mpv.conf b/config/implay/mpv.conf new file mode 100644 index 0000000..58234b5 --- /dev/null +++ b/config/implay/mpv.conf @@ -0,0 +1,141 @@ +# +# Example mpv configuration file +# +# Warning: +# +# The commented example options usually do _not_ set the default values. Call +# mpv with --list-options to see the default values for most options. There is +# no builtin or example mpv.conf with all the defaults. +# +# +# Configuration file settings and the command line options use the same +# underlying mechanisms. Most options can be put into the configuration file +# by dropping the preceding '--'. See the man page for a complete list of +# options. +# +# Lines starting with '#' are comments and are ignored. +# +# See the CONFIGURATION FILES section in the man page +# for a detailed description of the syntax. +# +# Profiles should be placed at the bottom of the configuration file to ensure +# that settings wanted as defaults are not restricted to specific profiles. + +################## +# video settings # +################## + +# Start in fullscreen mode by default. +#fs=yes + +# force starting with centered window +#geometry=50%:50% + +# don't allow a new window to have a size larger than 90% of the screen size +#autofit-larger=90%x90% + +# Do not close the window on exit. +#keep-open=yes + +# Do not wait with showing the video window until it has loaded. (This will +# resize the window once video is loaded. Also always shows a window with +# audio.) +#force-window=immediate + +# Disable the On Screen Controller (OSC). +osc=no + +# Keep the player window on top of all other windows. +#ontop=yes + +# Specify high quality video rendering preset (for --vo=gpu only) +# Can cause performance problems with some drivers and GPUs. +#profile=gpu-hq + +# Force video to lock on the display's refresh rate, and change video and audio +# speed to some degree to ensure synchronous playback - can cause problems +# with some drivers and desktop environments. +#video-sync=display-resample + +# Enable hardware decoding if available. Often, this does not work with all +# video outputs, but should work well with default settings on most systems. +# If performance or energy usage is an issue, forcing the vdpau or vaapi VOs +# may or may not help. +#hwdec=auto + +################## +# audio settings # +################## + +# Specify default audio device. You can list devices with: --audio-device=help +# The option takes the device string (the stuff between the '...'). +#audio-device=alsa/default + +# Do not filter audio to keep pitch when changing playback speed. +#audio-pitch-correction=no + +# Output 5.1 audio natively, and upmix/downmix audio with a different format. +#audio-channels=5.1 +# Disable any automatic remix, _if_ the audio output accepts the audio format. +# of the currently played file. See caveats mentioned in the manpage. +# (The default is "auto-safe", see manpage.) +#audio-channels=auto + +################## +# other settings # +################## + +# Pretend to be a web browser. Might fix playback with some streaming sites, +# but also will break with shoutcast streams. +#user-agent="Mozilla/5.0" + +# cache settings +# +# Use a large seekable RAM cache even for local input. +#cache=yes +# +# Use extra large RAM cache (needs cache=yes to make it useful). +#demuxer-max-bytes=500M +#demuxer-max-back-bytes=100M +# +# Disable the behavior that the player will pause if the cache goes below a +# certain fill size. +#cache-pause=no +# +# Store cache payload on the hard disk instead of in RAM. (This may negatively +# impact performance unless used for slow input such as network.) +#cache-dir=~/.cache/ +#cache-on-disk=yes + +# Display English subtitles if available. +#slang=en + +# Play Finnish audio if available, fall back to English otherwise. +#alang=fi,en + +# Change subtitle encoding. For Arabic subtitles use 'cp1256'. +# If the file seems to be valid UTF-8, prefer UTF-8. +# (You can add '+' in front of the codepage to force it.) +#sub-codepage=cp1256 + +# You can also include other configuration files. +#include=/path/to/the/file/you/want/to/include + +############ +# Profiles # +############ + +# The options declared as part of profiles override global default settings, +# but only take effect when the profile is active. + +# The following profile can be enabled on the command line with: --profile=eye-cancer + +#[eye-cancer] +#sharpen=5 + +# use opengl-hq video output for high-quality video rendering. +profile=gpu-hq +deband=no + +# Enable hardware decoding if available. +hwdec=auto diff --git a/config/implay/script-opts/modernf.conf b/config/implay/script-opts/modernf.conf new file mode 100644 index 0000000..fc1e940 --- /dev/null +++ b/config/implay/script-opts/modernf.conf @@ -0,0 +1,68 @@ +#show OSC when windowed? yes/no
+#showwindowed=
+
+#show OSC when fullscreen? yes/no
+#showfullscreen=
+
+#scaling of the controller when windowed
+scalewindowed=1
+
+#scaling of the controller when fullscreen
+scalefullscreen=1
+
+#scaling when rendered on a forced window
+scaleforcedwindow=2
+
+#scale the controller with the video? yes/no
+#vidscale=
+
+#duration in ms until the OSC hides if no mouse movement. enforced non-negative for the user but internally negative is 'always-on'.
+# hidetimeout=500
+
+#duration of fade out in ms 0=no fade
+#fadeduration=200
+
+#minimum amount of pixels the mouse has to move between ticks to make the OSC show up
+#minmousemove=3
+
+#use native mpv values and disable OSC internal track list management (and some functions that depend on it) yes/no
+#amaprogrammer=
+
+#default osc font
+#font='mpv-osd-symbols'
+
+#show seekrange overlay yes/no
+#seekrange=
+
+#transparency of seekranges
+#seekrangealpha=128
+
+#use keyframes when dragging the seekbar yes/no
+#seekbarkeyframes=
+
+#string compatible with property-expansion to be shown as OSC title
+#title='${media-title}'
+
+#show osc and no hide timeout on pause yes/no
+#showonpause=
+
+#display total time instead of remaining time? yes/no
+#timetotal=
+
+#only used at init to set visibility_mode(...) auto/always/never
+#visibility=
+
+#whether to show window controls auto/yes/no
+windowcontrols=no
+
+#whether to show mute button and volumne slider yes/no
+#volumecontrol=
+
+#volume bar show processd volume yes/no
+#processvolume=
+
+#eng=English chs=Chinese eng/chs
+#language=
+
+#alpha of the background box,0 (opaque) to 255 (fully transparent)
+boxalpha=80
diff --git a/config/implay/script-opts/osc.conf b/config/implay/script-opts/osc.conf new file mode 100644 index 0000000..40c796e --- /dev/null +++ b/config/implay/script-opts/osc.conf @@ -0,0 +1,3 @@ +scalewindowed=1.1458334 +hidetimeout=2000 +idlescreen=no diff --git a/config/implay/scripts/modernf.lua b/config/implay/scripts/modernf.lua new file mode 100644 index 0000000..5c32235 --- /dev/null +++ b/config/implay/scripts/modernf.lua @@ -0,0 +1,2320 @@ +-- Forked from maoiscat's modern.lua by Finn
+-- https://github.com/FinnRaze/mpv-osc-modern-f
+
+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 modernf.conf
+-- cycle visibility via input.conf: script-binding modernf/visibility
+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 = 500, -- duration in ms until the OSC hides if no
+ -- mouse movement. enforced non-negative for the
+ -- user, but internally negative is 'always-on'.
+ fadeduration = 200, -- duration of fade out in ms, 0 = no fade
+ minmousemove = 0, -- 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
+ 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
+ showonpause = true, -- show title and no hide timeout on pause
+ timetotal = false, -- display total time instead of remaining time?
+ timems = false, -- display timecodes with milliseconds
+ visibility = 'auto', -- only used at init to set visibility_mode(...)
+ windowcontrols = 'auto', -- whether to show window controls
+ volumecontrol = true, -- whether to show mute button and volumne slider
+ processvolume = false, -- volue slider show processd volume
+ language = 'chs', -- eng=English, chs=Chinese
+ boxalpha = 180
+}
+
+-- Localization
+local language = {
+ ['eng'] = {
+ welcome = '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 = '{\\fs48}拖拽文件或url到此窗口播放', -- 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, 'modernf', 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&}',
+ VolumebarBg = '{\\blur0\\bord0\\1c&H999999&}',
+ VolumebarFg = '{\\blur1\\bord1\\1c&HFFFFFF&}',
+ 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\\fs36\\q2\\fn' .. user_opts.font .. '}',
+ WinCtrl = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs20\\fnmpv-osd-symbols}',
+ elementDown = '{\\1c&H999999&}',
+ wcBar = "{\\1c&H000000}",
+}
+
+-- 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
+ tc_ms = user_opts.timems, -- should the timecodes display their time with milliseconds
+ 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'),
+ mute = false,
+ lastvisibility = user_opts.visibility, -- save last visibility on pause if showonpause
+ sys_volume, --system volume
+ proc_volume, --processed volume
+}
+
+local thumbfast = {
+ width = 0,
+ height = 0,
+ disabled = true,
+ available = false
+}
+
+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
+
+-- set volume
+function set_volume(val)
+ if user_opts.processvolume then
+ val = 10*math.sqrt(val)
+ end
+ mp.commandv('set', 'volume', val)
+ mp.commandv('add', 'volume', 0) --this prevent volume exceeds limit
+end
+--
+-- Tracklist Management
+--
+
+local nicetypes = {video = texts.video, audio = texts.audio, sub = texts.subtitle}
+
+-- updates the OSC internal playlists, should be run each time the track-layout changes
+function update_tracklist()
+ local tracktable = mp.get_property_native('track-list', {})
+
+ -- by osc_id
+ tracks_osc = {}
+ tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
+ -- by mpv_id
+ tracks_mpv = {}
+ tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
+ for n = 1, #tracktable do
+ if not (tracktable[n].type == 'unknown') then
+ local type = tracktable[n].type
+ local mpv_id = tonumber(tracktable[n].id)
+
+ -- by osc_id
+ table.insert(tracks_osc[type], tracktable[n])
+
+ -- by mpv_id
+ tracks_mpv[type][mpv_id] = tracktable[n]
+ tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
+ end
+ end
+end
+
+-- return a nice list of tracks of the given type (video, audio, sub)
+function get_tracklist(type)
+ local msg = texts.available .. nicetypes[type] .. texts.track
+ if #tracks_osc[type] == 0 then
+ msg = msg .. texts.none
+ else
+ for n = 1, #tracks_osc[type] do
+ local track = tracks_osc[type][n]
+ local lang, title, selected = 'unknown', '', '○'
+ if not(track.lang == nil) then lang = track.lang end
+ if not(track.title == nil) then title = track.title end
+ if (track.id == tonumber(mp.get_property(type))) then
+ selected = '●'
+ end
+ msg = msg..'\n'..selected..' '..n..': ['..lang..'] '..title
+ end
+ end
+ return msg
+end
+
+-- relatively change the track of given <type> by <next> tracks
+ --(+1 -> next, -1 -> previous)
+function set_track(type, next)
+ local current_track_mpv, current_track_osc
+ if (mp.get_property(type) == 'no') then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.get_property(type))
+ current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
+ end
+ local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
+ local new_track_mpv
+ if new_track_osc == 0 then
+ new_track_mpv = 'no'
+ else
+ new_track_mpv = tracks_osc[type][new_track_osc].id
+ end
+
+ mp.commandv('set', type, new_track_mpv)
+
+-- if (new_track_osc == 0) then
+-- show_message(nicetypes[type] .. ' Track: none')
+-- else
+-- show_message(nicetypes[type] .. ' Track: '
+-- .. new_track_osc .. '/' .. #tracks_osc[type]
+-- .. ' ['.. (tracks_osc[type][new_track_osc].lang or 'unknown') ..'] '
+-- .. (tracks_osc[type][new_track_osc].title or ''))
+-- end
+end
+
+-- get the currently selected track of <type>, OSC-style counted
+function get_track(type)
+ local track = mp.get_property(type)
+ if track ~= 'no' and track ~= nil then
+ local tr = tracks_mpv[type][tonumber(track)]
+ if tr then
+ return tr.osc_id
+ end
+ end
+ return 0
+end
+
+-- WindowControl helpers
+function window_controls_enabled()
+ val = user_opts.windowcontrols
+ if val == 'auto' then
+ return (not state.border) or state.fullscreen
+ else
+ return val ~= 'no'
+ end
+end
+
+--
+-- Element Management
+--
+
+local elements = {}
+
+function prepare_elements()
+
+ -- remove elements without layout or invisble
+ local elements2 = {}
+ for n, element in pairs(elements) do
+ if not (element.layout == nil) and (element.visible) then
+ table.insert(elements2, element)
+ end
+ end
+ elements = elements2
+
+ function elem_compare (a, b)
+ return a.layout.layer < b.layout.layer
+ end
+
+ table.sort(elements, elem_compare)
+
+
+ for _,element in pairs(elements) do
+
+ local elem_geo = element.layout.geometry
+
+ -- Calculate the hitbox
+ local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
+ element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
+
+ local style_ass = assdraw.ass_new()
+
+ -- prepare static elements
+ style_ass:append('{}') -- hack to troll new_event into inserting a \n
+ style_ass:new_event()
+ style_ass:pos(elem_geo.x, elem_geo.y)
+ style_ass:an(elem_geo.an)
+ style_ass:append(element.layout.style)
+
+ element.style_ass = style_ass
+
+ local static_ass = assdraw.ass_new()
+
+
+ if (element.type == 'box') then
+ --draw box
+ static_ass:draw_start()
+ ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
+ element.layout.box.radius, element.layout.box.hexagon)
+ static_ass:draw_stop()
+
+ elseif (element.type == 'slider') then
+ --draw static slider parts
+ local slider_lo = element.layout.slider
+ -- calculate positions of min and max points
+ element.slider.min.ele_pos = 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
+ local rh = 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 element.slider.seekRangesF ~= nil then seekRanges = element.slider.seekRangesF() 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)
+
+ -- thumbnail
+ if element.thumbnail and not thumbfast.disabled then
+ local osd_w = mp.get_property_number("osd-width")
+ if osd_w then
+ local r_w, r_h = get_virt_scale_factor()
+
+ local thumbPad = 4
+ local thumbMarginX = 18 / r_w
+ local thumbMarginY = 30
+ local tooltipBgColor = "FFFFFF"
+ local tooltipBgAlpha = 80
+ local thumbX = math.min(osd_w - thumbfast.width - thumbMarginX, math.max(thumbMarginX, tx / r_w - thumbfast.width / 2))
+ local thumbY = ((ty - thumbMarginY) / r_h - thumbfast.height)
+
+ thumbX = math.floor(thumbX + 0.5)
+ thumbY = math.floor(thumbY + 0.5)
+
+ elem_ass:new_event()
+ elem_ass:pos(thumbX * r_w, ty - thumbMarginY - thumbfast.height * r_h)
+ elem_ass:append(osc_styles.Tooltip)
+ elem_ass:draw_start()
+ elem_ass:rect_cw(-thumbPad * r_w, -thumbPad * r_h, (thumbfast.width + thumbPad) * r_w, (thumbfast.height + thumbPad) * r_h)
+ elem_ass:draw_stop()
+
+ mp.commandv("script-message-to", "thumbfast", "thumb",
+ mp.get_property_number("duration", 0) * (sliderpos / 100),
+ thumbX,
+ thumbY
+ )
+ end
+ end
+ else
+ if element.thumbnail and thumbfast.available then
+ mp.commandv("script-message-to", "thumbfast", "clear")
+ end
+ end
+ end
+
+ elseif (element.type == 'button') then
+
+ local buttontext
+ if type(element.content) == 'function' then
+ buttontext = element.content() -- function objects
+ elseif 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() + 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-14, 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*1 / 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
+
+ local button_geo={
+ x = controlbox_left+69,
+ y = button_y,
+ an = 5,
+ w = 142,
+ h = wc_geo.h,
+ }
+
+ -- Background Bar
+ new_element("wcbar", "box")
+ lo = add_layout("wcbar")
+ lo.geometry = button_geo
+ lo.layer = 10
+ lo.style = osc_styles.wcBar
+ lo.alpha[1] = user_opts.boxalpha
+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, osc_param.playresy-200, osc_param.playresx, osc_param.playresy)
+ add_area('showhide_wc', osc_param.playresx*0.67, 0, osc_param.playresx, 48)
+
+ -- 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] = user_opts.boxalpha
+
+ --
+ -- Alignment
+ --
+ local refX = 0
+ local refY = posY
+ local geo
+
+ --
+ -- Seekbar
+ --
+ new_element('seekbarbg', 'box')
+ lo = add_layout('seekbarbg')
+ lo.geometry = {x = osc_geo.w / 2 , 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 = osc_geo.w / 2 , 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
+ --
+ -- Volumebar
+ --
+ lo = new_element('volumebarbg', 'box')
+ lo.visible = (osc_param.playresx >= 750) and user_opts.volumecontrol
+ lo = add_layout('volumebarbg')
+ lo.geometry = {x = osc_geo.w - 257, y = refY - 40, an = 4, w = 80, h = 2}
+ lo.layer = 13
+ lo.style = osc_styles.VolumebarBg
+
+ lo = add_layout('volumebar')
+ lo.geometry = {x = osc_geo.w - 257, y = refY - 40, an = 4, w = 80, h = 8}
+ lo.style = osc_styles.VolumebarFg
+ lo.slider.gap = 3
+ lo.slider.tooltip_style = osc_styles.Tooltip
+ lo.slider.tooltip_an = 2
+
+ -- buttons
+ lo = add_layout('pl_prev')
+ lo.geometry = {x = refX + 87, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl2
+
+ lo = add_layout('playpause')
+ lo.geometry = {x = refX + 37, y = refY - 40 , an = 5, w = 45, h = 45}
+ lo.style = osc_styles.Ctrl1
+
+ lo = add_layout('pl_next')
+ lo.geometry = {x = refX + 137, 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
+
+ -- Option
+ lo = add_layout('cy_sub')
+ lo.geometry = {x = refX + 187, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl3
+ lo.visible = (osc_param.playresx >= 540)
+
+ lo = add_layout('cy_audio')
+ lo.geometry = {x = refX + 237, y = refY - 40 , an = 5, w = 30, h = 24}
+ lo.style = osc_styles.Ctrl3
+ lo.visible = (osc_param.playresx >= 600)
+
+ lo = add_layout('vol_ctrl')
+ lo.geometry = {x = osc_geo.w - 277, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+ lo.visible = (osc_param.playresx >= 650)
+
+ 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.visible = (osc_param.playresx >= 500)
+
+ lo = add_layout('ontop')
+ lo.geometry = {x = osc_geo.w - 137, y = refY - 40, an = 5, w = 24, h = 24}
+ lo.style = osc_styles.Ctrl3
+ lo.visible = (osc_param.playresx >= 600)
+
+ 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
+ lo.visible = (osc_param.playresx >= 600)
+
+ 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
+ lo.button.maxchars = geo.w / 23
+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 () mp.commandv('script-binding', 'playlistmanager/showplaylist') 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 () mp.commandv('script-binding', 'playlistmanager/showplaylist') end
+
+
+ --play control buttons
+ --playpause
+ ne = new_element('playpause', 'button')
+
+ ne.content = function ()
+ if mp.get_property('pause') == 'no' 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_right_down'] =
+ --function () mp.command('add chapter -1') end
+ function () mp.commandv('add','chapter', -1) end
+ ne.eventresponder['shift+mbtn_left_down'] =
+ function () mp.commandv('frame-back-step') end
+ ne.eventresponder['mbtn_left_down'] =
+ --function () mp.command('seek -90') end
+ function () mp.commandv('seek', -90, 'relative', 'keyframes') end
+
+ --skipfrwd
+ ne = new_element('skipfrwd', 'button')
+
+ ne.softrepeat = true
+ ne.content = '\xEF\x8E\x9F'
+ ne.eventresponder['mbtn_right_down'] =
+ --function () mp.command('add chapter +1') end
+ function () mp.commandv('add','chapter', 1) end
+ ne.eventresponder['shift+mbtn_left_down'] =
+ function () mp.commandv('frame-step') end
+ ne.eventresponder['mbtn_left_down'] =
+ --function () mp.command('seek +87') end
+ function () mp.commandv('seek', 87, 'relative', 'keyframes') end
+
+ --
+ update_tracklist()
+
+ --cy_audio
+ ne = new_element('cy_audio', 'button')
+ ne.enabled = (#tracks_osc.audio > 0)
+ ne.visible = (osc_param.playresx >= 600)
+ 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['shift+mbtn_left_down'] =
+ 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 >= 540)
+ ne.content = '\xEF\x8C\xA4'
+ 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['shift+mbtn_left_down'] =
+ function () show_message(get_tracklist('sub')) end
+
+ -- vol_ctrl
+ ne = new_element('vol_ctrl', 'button')
+ ne.enabled = (get_track('audio')>0)
+ ne.visible = (osc_param.playresx >= 650) and user_opts.volumecontrol
+ ne.content = function()
+ local volume = mp.get_property_number("volume", 0)
+ local mute = mp.get_property_native("mute")
+ local volicon = {'\xEF\x8E\xBA',
+ '\xEF\x8E\xB9', '\xEF\x8E\xBC'}
+ if volume == 0 or mute then
+ return '\xEF\x8E\xBB'
+ else
+ return volicon[math.min(3,math.ceil(volume / (100/3)))]
+ end
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('cycle', 'mute') 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_ontop
+ ne = new_element('ontop', 'button')
+ ne.content = function ()
+ if mp.get_property('ontop') == 'no' then
+ return ('\xEF\x86\x8B')
+ else
+ return ('\xEF\x86\x8C')
+ end
+ end
+ --ne.visible = (osc_param.playresx >= 540)
+ ne.eventresponder['mbtn_left_up'] =
+ function () mp.commandv('cycle', 'ontop') 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
+ ne.eventresponder['mbtn_right_up'] =
+ function () mp.commandv('script-binding', 'stats/display-page-4') 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.playresx >= 700 --and user_opts.showonpause
+
+ --seekbar
+ ne = new_element('seekbar', 'slider')
+
+ ne.enabled = not (mp.get_property('percent-pos') == nil)
+ ne.thumbnail = true
+ 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
+ local possec = duration * (pos / 100)
+ local chapters = mp.get_property_native('chapter-list', {})
+ if #chapters > 0 then
+ local ch = #chapters
+ local i
+ for i = 1, #chapters do
+ if chapters[i].time / duration * 100 >= pos then
+ ch = i - 1
+ break
+ end
+ end
+ if ch == 0 then
+ return string.format('[%s] [0/%d]', mp.format_time(possec), #chapters)
+ elseif chapters[ch].title then
+ return string.format('[%s] [%d/%d][%s]', mp.format_time(possec), ch, #chapters, chapters[ch].title)
+ end
+ end
+ 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
+
+ --volumebar
+ ne = new_element('volumebar', 'slider')
+ ne.visible = (osc_param.playresx >= 700) and user_opts.volumecontrol
+ ne.enabled = (get_track('audio')>0)
+ ne.slider.tooltipF =
+ function (pos)
+ local refpos = state.proc_volume
+ if refpos > 100 then refpos = 100 end
+ if pos+3 >= refpos and pos-3 <= refpos then
+ return string.format('%d', state.proc_volume)
+ else
+ return ''
+ end
+ end
+ ne.slider.markerF = nil
+ ne.slider.seekRangesF = nil
+ ne.slider.posF =
+ function ()
+ return state.proc_volume
+ end
+ ne.eventresponder['mouse_move'] =
+ function (element)
+ if not element.state.mbtnleft then return end
+ local seekto = get_slider_value(element)
+ if (element.state.lastseek == nil) or
+ (not (element.state.lastseek == seekto)) then
+ set_volume(seekto)
+ element.state.lastseek = seekto
+ end
+ end
+ ne.eventresponder['mbtn_left_down'] = --exact seeks on single clicks
+ function (element)
+ local seekto = get_slider_value(element)
+ set_volume(seekto)
+ element.state.mbtnleft = true
+ end
+ ne.eventresponder['mbtn_left_up'] =
+ function (element)
+ element.state.mbtnleft = false
+ end
+ ne.eventresponder['reset'] =
+ function (element) element.state.lastseek = nil end
+ ne.eventresponder['wheel_up_press'] =
+ function (element)
+ set_volume(state.proc_volume+5)
+ end
+ ne.eventresponder['wheel_down_press'] =
+ function (element)
+ set_volume(state.proc_volume-5)
+ end
+ -- tc_left (current pos)
+ ne = new_element('tc_left', 'button')
+ ne.content = function ()
+ if (state.tc_ms) then
+ return (mp.get_property_osd('playback-time/full'))
+ else
+ return (mp.get_property_osd('playback-time'))
+ end
+ end
+ ne.eventresponder['mbtn_left_up'] = function ()
+ state.tc_ms = not state.tc_ms
+ request_init()
+ 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
+ if (state.tc_ms) then
+ return ('-'..mp.get_property_osd('playtime-remaining/full'))
+ else
+ return ('-'..mp.get_property_osd('playtime-remaining'))
+ end
+ else
+ if (state.tc_ms) then
+ return (mp.get_property_osd('duration/full'))
+ else
+ return (mp.get_property_osd('duration'))
+ end
+ 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.showonpause and user_opts.visibility~='never' 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, osd_aspect = mp.get_osd_size()
+ osd_w, osd_h = 1024*osd_aspect, 1024
+ 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+140)
+ 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('mute', 'bool',
+ function(name, val)
+ state.mute = val
+ end
+)
+mp.observe_property('volume', 'number',
+ function(name, val)
+ state.sys_volume = val
+ if user_opts.processvolume then
+ state.proc_volume = val*val/100
+ else
+ state.proc_volume = val
+ end
+ 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},
+ {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
+ function(e) process_event("shift+mbtn_left", "down") 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 == "cycle" then
+ if not state.enabled then
+ mode = "auto"
+ elseif user_opts.visibility ~= "always" then
+ mode = "always"
+ else
+ mode = "never"
+ end
+ end
+
+ if mode == 'auto' then
+ always_on(false)
+ enable_osc(true)
+ elseif mode == 'always' then
+ enable_osc(true)
+ always_on(true)
+ elseif mode == 'never' then
+ enable_osc(false)
+ else
+ msg.warn('Ignoring unknown visibility mode \'' .. mode .. '\'')
+ return
+ end
+
+ user_opts.visibility = mode
+ utils.shared_script_property_set("osc-visibility", mode)
+
+ if not no_osd and tonumber(mp.get_property('osd-level')) >= 1 then
+ mp.osd_message('OSC visibility: ' .. mode)
+ end
+
+ -- Reset the input state on a mode change. The input state will be
+ -- 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
+
+function idlescreen_visibility(mode, no_osd)
+ if mode == "cycle" then
+ if user_opts.idlescreen then
+ mode = "no"
+ else
+ mode = "yes"
+ end
+ end
+
+ if mode == "yes" then
+ user_opts.idlescreen = true
+ else
+ user_opts.idlescreen = false
+ end
+
+ utils.shared_script_property_set("osc-idlescreen", mode)
+
+ if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
+ mp.osd_message("OSC logo visibility: " .. tostring(mode))
+ end
+
+ request_tick()
+end
+
+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)
+
+mp.register_script_message("osc-idlescreen", idlescreen_visibility)
+
+mp.register_script_message("thumbfast-info", function(json)
+ local data = utils.parse_json(json)
+ if type(data) ~= "table" or not data.width or not data.height then
+ msg.error("thumbfast-info: received json didn't produce a table with thumbnail information")
+ else
+ thumbfast = data
+ end
+end)
+
+set_virt_mouse_area(0, 0, 0, 0, 'input')
+set_virt_mouse_area(0, 0, 0, 0, 'window-controls')
|